/** * Config controller -- load, normalize, save, verify. * * Settings are stored in ~/.pi/agent/settings.json under the * "pi-credential-vault" key, following the same pattern as pi-multicodex. * * Pi does not expose a getSettings() API to extensions. Both pi-multicodex * and pi-context-optimizer read/write settings.json directly. */ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { getAgentDir } from "@mariozechner/pi-coding-agent"; import type { BackendStatus, BackendType, CredentialBackend, VaultConfig, } from "../types.js"; import { DEFAULT_CONFIG, SETTINGS_KEY } from "./defaults.js"; import { clearBackendCache, createBackend, getPassthroughBackend, } from "../backends/registry.js"; const SETTINGS_FILE = join(getAgentDir(), "settings.json"); const VALID_BACKENDS: ReadonlySet = new Set([ "age", "keychain", "passthrough", ]); // --------------------------------------------------------------------------- // Settings file I/O (same pattern as pi-multicodex) // --------------------------------------------------------------------------- function readSettingsFile(): Record { try { if (!existsSync(SETTINGS_FILE)) { return {}; } const raw = readFileSync(SETTINGS_FILE, "utf-8"); const parsed: unknown = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return {}; } return parsed as Record; } catch { return {}; } } function writeSettingsFile(settings: Record): void { const dir = dirname(SETTINGS_FILE); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf-8"); } // --------------------------------------------------------------------------- // Normalization // --------------------------------------------------------------------------- function normalizeBackendType(raw: unknown): BackendType { if (typeof raw === "string" && VALID_BACKENDS.has(raw)) { return raw as BackendType; } return DEFAULT_CONFIG.backend; } function normalizeManagedProviders( raw: unknown, ): "all" | readonly string[] { if (raw === "all") { return "all"; } if (Array.isArray(raw)) { return raw.filter( (item: unknown): item is string => typeof item === "string", ); } return DEFAULT_CONFIG.managedProviders; } function normalizeStringArray(raw: unknown): readonly string[] { if (Array.isArray(raw)) { return raw.filter( (item: unknown): item is string => typeof item === "string", ); } return []; } function normalizeConfig(raw: unknown): VaultConfig { if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return { ...DEFAULT_CONFIG }; } const obj = raw as Record; return { backend: normalizeBackendType(obj.backend), managedProviders: normalizeManagedProviders(obj.managedProviders), excludeProviders: normalizeStringArray(obj.excludeProviders), age: obj.age && typeof obj.age === "object" && !Array.isArray(obj.age) ? (obj.age as VaultConfig["age"]) : DEFAULT_CONFIG.age, keychain: obj.keychain && typeof obj.keychain === "object" && !Array.isArray(obj.keychain) ? (obj.keychain as VaultConfig["keychain"]) : DEFAULT_CONFIG.keychain, }; } // --------------------------------------------------------------------------- // Controller // --------------------------------------------------------------------------- export interface VaultController { getConfig(): VaultConfig; setConfig(next: VaultConfig): void; getBackend(): CredentialBackend; getBackendStatus(): Promise; isProviderManaged(providerId: string): boolean; reload(): void; } export function createVaultController(): VaultController { let config: VaultConfig = DEFAULT_CONFIG; let backend: CredentialBackend = getPassthroughBackend(); function loadConfig(): void { const settings = readSettingsFile(); const raw = settings[SETTINGS_KEY]; config = normalizeConfig(raw); clearBackendCache(); try { backend = createBackend(config.backend, config); } catch { backend = getPassthroughBackend(); } } function saveConfig(next: VaultConfig): void { const normalized = normalizeConfig(next); const settings = readSettingsFile(); settings[SETTINGS_KEY] = normalized; writeSettingsFile(settings); config = normalized; clearBackendCache(); try { backend = createBackend(normalized.backend, normalized); } catch { backend = getPassthroughBackend(); } } // Initial load loadConfig(); return { getConfig() { return config; }, setConfig(next: VaultConfig) { saveConfig(next); }, getBackend() { return backend; }, async getBackendStatus(): Promise { return backend.check(); }, isProviderManaged(providerId: string): boolean { const excluded = config.excludeProviders ?? []; if (excluded.includes(providerId)) { return false; } if (config.managedProviders === "all") { return true; } return config.managedProviders.includes(providerId); }, reload() { loadConfig(); }, }; }