multica/src/agent/credentials.ts

202 lines
5.4 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import JSON5 from "json5";
import { DATA_DIR } from "../shared/paths.js";
type ProviderConfig = {
apiKey?: string | undefined;
baseUrl?: string | undefined;
model?: string | undefined;
};
type ToolConfig = {
apiKey?: string | undefined;
baseUrl?: string | undefined;
model?: string | undefined;
};
export type CredentialsConfig = {
version?: number | undefined;
llm?: {
provider?: string | undefined;
providers?: Record<string, ProviderConfig> | undefined;
} | undefined;
tools?: Record<string, ToolConfig> | undefined;
};
type SkillsEnvConfig = {
env?: Record<string, string> | undefined;
};
const DEFAULT_CREDENTIALS_PATH = join(DATA_DIR, "credentials.json5");
const DEFAULT_SKILLS_ENV_PATH = join(DATA_DIR, "skills.env.json5");
function expandHome(value: string): string {
if (value === "~") return homedir();
if (value.startsWith("~/")) {
return join(homedir(), value.slice(2));
}
return value;
}
function isTestEnv(): boolean {
return (
process.env.NODE_ENV === "test" ||
process.env.VITEST !== undefined ||
process.env.VITEST_WORKER_ID !== undefined
);
}
function isString(value: unknown): value is string {
return typeof value === "string";
}
function setEnvValue(target: Record<string, string>, key: string, value: unknown): void {
if (isString(value)) {
target[key] = value;
}
}
function applyEnvMap(target: Record<string, string>, env?: Record<string, string>): void {
if (!env) return;
for (const [key, value] of Object.entries(env)) {
setEnvValue(target, key, value);
}
}
export function getCredentialsPath(): string {
const raw = process.env.SMC_CREDENTIALS_PATH ?? DEFAULT_CREDENTIALS_PATH;
return expandHome(raw);
}
export function getSkillsEnvPath(): string {
const raw = process.env.SMC_SKILLS_ENV_PATH ?? DEFAULT_SKILLS_ENV_PATH;
return expandHome(raw);
}
export class CredentialManager {
private corePath: string | null = null;
private skillsPath: string | null = null;
private disabledState: boolean | null = null;
private coreConfig: CredentialsConfig | null = null;
private skillsConfig: SkillsEnvConfig | null = null;
private resolvedSkillsEnv: Record<string, string> | null = null;
private isDisabled(): boolean {
if (process.env.SMC_CREDENTIALS_DISABLE === "1") return true;
return isTestEnv();
}
private loadCore(): void {
const path = getCredentialsPath();
const disabled = this.isDisabled();
if (this.corePath === path && this.disabledState === disabled && this.coreConfig) {
return;
}
this.corePath = path;
this.disabledState = disabled;
this.coreConfig = null;
if (disabled) return;
if (!existsSync(path)) return;
const raw = readFileSync(path, "utf8");
try {
this.coreConfig = JSON5.parse(raw) as CredentialsConfig;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to parse credentials file (${path}): ${message}`);
}
}
private loadSkillsEnv(): void {
const path = getSkillsEnvPath();
const disabled = this.isDisabled();
if (this.skillsPath === path && this.disabledState === disabled && this.resolvedSkillsEnv) {
return;
}
this.skillsPath = path;
this.disabledState = disabled;
this.skillsConfig = null;
this.resolvedSkillsEnv = null;
if (disabled) return;
if (!existsSync(path)) return;
const raw = readFileSync(path, "utf8");
try {
this.skillsConfig = JSON5.parse(raw) as SkillsEnvConfig;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to parse skills env file (${path}): ${message}`);
}
}
private buildSkillsEnv(): Record<string, string> {
const env: Record<string, string> = {};
if (!this.skillsConfig) return env;
applyEnvMap(env, this.skillsConfig.env);
return env;
}
private getResolvedSkillsEnv(): Record<string, string> {
this.loadSkillsEnv();
if (!this.resolvedSkillsEnv) {
this.resolvedSkillsEnv = this.buildSkillsEnv();
}
return this.resolvedSkillsEnv;
}
getLlmProvider(): string | undefined {
this.loadCore();
return this.coreConfig?.llm?.provider;
}
getLlmProviderConfig(provider: string): ProviderConfig | undefined {
this.loadCore();
return this.coreConfig?.llm?.providers?.[provider];
}
getToolConfig(toolName: string): ToolConfig | undefined {
this.loadCore();
return this.coreConfig?.tools?.[toolName];
}
getEnv(name: string): string | undefined {
const resolved = this.getResolvedSkillsEnv();
if (Object.prototype.hasOwnProperty.call(resolved, name)) {
return resolved[name];
}
return process.env[name];
}
hasEnv(name: string): boolean {
const resolved = this.getResolvedSkillsEnv();
if (Object.prototype.hasOwnProperty.call(resolved, name)) {
return true;
}
return name in process.env;
}
getResolvedEnvSnapshot(): Record<string, string> {
return { ...this.getResolvedSkillsEnv() };
}
reset(): void {
this.corePath = null;
this.skillsPath = null;
this.disabledState = null;
this.coreConfig = null;
this.skillsConfig = null;
this.resolvedSkillsEnv = null;
}
}
export const credentialManager = new CredentialManager();