mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 03:01:56 +00:00
Support shell command execution for API key resolution in models.json (#762)
* Support shell command execution for API key resolution in models.json Add ! prefix support to apiKey field in models.json to execute shell commands and use stdout as the API key. This allows users to store API keys in secure credential managers like macOS Keychain, 1Password, Bitwarden, or HashiCorp Vault. Example: "apiKey": "!security find-generic-password -ws 'anthropic'" The apiKey field now supports three formats: - !command - executes shell command, uses trimmed stdout - ENV_VAR_NAME - uses environment variable value - literal - uses value directly fixes #697 * feat(coding-agent): cache API key command results for process lifetime Shell commands (! prefix) are now executed once and cached. Environment variables and literal values are not cached, so changes are picked up. Addresses review feedback on #762. --------- Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
parent
a67f6f9916
commit
def9e4e9a9
5 changed files with 315 additions and 14 deletions
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "@mariozechner/pi-ai";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import AjvModule from "ajv";
|
||||
import { execSync } from "child_process";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import type { AuthStorage } from "./auth-storage.js";
|
||||
|
||||
|
|
@ -99,14 +100,47 @@ function emptyCustomModelsResult(error?: string): CustomModelsResult {
|
|||
return { models: [], replacedProviders: new Set(), overrides: new Map(), error };
|
||||
}
|
||||
|
||||
// Cache for shell command results (persists for process lifetime)
|
||||
const commandResultCache = new Map<string, string | undefined>();
|
||||
|
||||
/**
|
||||
* Resolve an API key config value to an actual key.
|
||||
* Checks environment variable first, then treats as literal.
|
||||
* - If starts with "!", executes the rest as a shell command and uses stdout (cached)
|
||||
* - Otherwise checks environment variable first, then treats as literal (not cached)
|
||||
*/
|
||||
function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
||||
if (keyConfig.startsWith("!")) {
|
||||
return executeApiKeyCommand(keyConfig);
|
||||
}
|
||||
const envValue = process.env[keyConfig];
|
||||
if (envValue) return envValue;
|
||||
return keyConfig;
|
||||
return envValue || keyConfig;
|
||||
}
|
||||
|
||||
function executeApiKeyCommand(commandConfig: string): string | undefined {
|
||||
if (commandResultCache.has(commandConfig)) {
|
||||
return commandResultCache.get(commandConfig);
|
||||
}
|
||||
|
||||
const command = commandConfig.slice(1);
|
||||
let result: string | undefined;
|
||||
try {
|
||||
const output = execSync(command, {
|
||||
encoding: "utf-8",
|
||||
timeout: 10000,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
result = output.trim() || undefined;
|
||||
} catch {
|
||||
result = undefined;
|
||||
}
|
||||
|
||||
commandResultCache.set(commandConfig, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Clear the API key command cache. Exported for testing. */
|
||||
export function clearApiKeyCache(): void {
|
||||
commandResultCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue