From 4d142eac1fd02fd819f6bf4db6bfdc6fb3c4cd7c Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 31 Mar 2026 04:02:33 +0530 Subject: [PATCH] add credential resolver for OpenClaw key sharing --- packages/cli/src/lib/credential-resolver.ts | 114 ++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 packages/cli/src/lib/credential-resolver.ts diff --git a/packages/cli/src/lib/credential-resolver.ts b/packages/cli/src/lib/credential-resolver.ts new file mode 100644 index 000000000..53506ac97 --- /dev/null +++ b/packages/cli/src/lib/credential-resolver.ts @@ -0,0 +1,114 @@ +/** + * Shared credential resolver — resolves API keys from multiple sources. + * + * Resolution order per key: + * 1. Environment variable (already set in process.env) + * 2. OpenClaw config (~/.openclaw/openclaw.json → keys section) + * + * Call `applyOpenClawCredentials()` early in CLI startup so spawned agent + * sessions inherit the resolved values via the parent process environment. + */ + +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +/** Keys that AO agents commonly need and OpenClaw may already store. */ +const RESOLVABLE_KEYS = [ + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "GITHUB_TOKEN", + "GOOGLE_API_KEY", + "OPENROUTER_API_KEY", +] as const; + +type ResolvableKey = (typeof RESOLVABLE_KEYS)[number]; + +interface ResolvedCredential { + key: ResolvableKey; + value: string; + source: "env" | "openclaw"; +} + +function readOpenClawKeys(): Record { + const keys: Record = {}; + + const configPath = join(homedir(), ".openclaw", "openclaw.json"); + if (!existsSync(configPath)) return keys; + + try { + const raw = readFileSync(configPath, "utf-8"); + const config = JSON.parse(raw) as Record; + + // OpenClaw stores keys in a top-level "keys" object: + // { "keys": { "ANTHROPIC_API_KEY": "sk-ant-...", ... } } + const keysSection = config.keys; + if (keysSection && typeof keysSection === "object") { + for (const [k, v] of Object.entries(keysSection as Record)) { + if (typeof v === "string" && v.length > 0) { + keys[k] = v; + } + } + } + + // Also check top-level env / environment block (some OpenClaw setups): + // { "env": { "ANTHROPIC_API_KEY": "sk-ant-..." } } + const envSection = config.env ?? config.environment; + if (envSection && typeof envSection === "object") { + for (const [k, v] of Object.entries(envSection as Record)) { + if (typeof v === "string" && v.length > 0 && !(k in keys)) { + keys[k] = v; + } + } + } + } catch { + // Malformed config — silently skip. + } + + return keys; +} + +/** + * Resolve a single credential from all available sources. + * Returns the value and where it came from, or null if not found anywhere. + */ +export function resolveCredential(key: ResolvableKey): ResolvedCredential | null { + const envValue = process.env[key]; + if (envValue && envValue.length > 0) { + return { key, value: envValue, source: "env" }; + } + + const openclawKeys = readOpenClawKeys(); + const openclawValue = openclawKeys[key]; + if (openclawValue) { + return { key, value: openclawValue, source: "openclaw" }; + } + + return null; +} + +/** + * Populate `process.env` with API keys found in OpenClaw config that are + * not already set in the environment. This makes them available to all + * spawned agent sessions (tmux inherits the parent env). + * + * Returns the list of keys that were injected from OpenClaw. + */ +export function applyOpenClawCredentials(): ResolvedCredential[] { + const openclawKeys = readOpenClawKeys(); + const applied: ResolvedCredential[] = []; + + for (const key of RESOLVABLE_KEYS) { + if (process.env[key] && process.env[key]!.length > 0) continue; + + const value = openclawKeys[key]; + if (value) { + process.env[key] = value; + applied.push({ key, value, source: "openclaw" }); + } + } + + return applied; +} + +export { RESOLVABLE_KEYS, type ResolvableKey, type ResolvedCredential };