UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

330 lines (329 loc) • 15 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { resolveSecret } from "../../core/config/config.js"; import { deepMergeConfig } from "../../core/config/deep-merge.js"; import { SECRET_STORE_REFERENCE_PATTERN } from "../../core/config/schema/primitives.js"; import { ConfigError } from "../../core/errors.js"; import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js"; import { collectSensitiveValues } from "../../core/redaction.js"; import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js"; import { getHarness } from "../harnesses/index.js"; import { DEFAULT_LLM_TIMEOUT_MS } from "./config.js"; import { getBuiltinAgentProfile, OPENCODE_SDK_SERVER_BIN } from "./profiles.js"; const LLM_CONNECTION_FIELDS = [ "provider", "endpoint", "model", "temperature", "maxTokens", "supportsJsonSchema", "extraParams", "contextLength", "enableThinking", "reasoningEffort", ]; function envName(reference) { const match = /^\$(?:\{)?([A-Za-z_][A-Za-z0-9_]*)(?:\})?$/.exec(reference); return match?.[1]; } function expandHomePath(filePath) { return filePath.startsWith("~") ? path.join(os.homedir(), filePath.slice(1)) : filePath; } /** Trim exactly one trailing newline (`\n` or `\r\n`) — never interior whitespace. */ function trimTrailingNewline(raw) { return raw.replace(/\r?\n$/, ""); } /** Read a file-backed credential; errors name the engine and path, never the contents. */ function readApiKeyFile(engineName, filePath) { let raw; try { raw = fs.readFileSync(filePath, "utf8"); } catch (err) { const code = err instanceof Error ? err.code : undefined; const reason = code === "ENOENT" ? "does not exist" : "could not be read"; throw new ConfigError(`Engine "${engineName}" apiKeyFile ${reason}: ${filePath}`, "INVALID_CONFIG_FILE"); } const value = trimTrailingNewline(raw); if (value.length === 0) { throw new ConfigError(`Engine "${engineName}" apiKeyFile is empty: ${filePath}`, "INVALID_CONFIG_FILE"); } return value; } /** Best-effort, non-throwing read of a file-backed credential for redaction and health. */ export function lookupApiKeyFileValue(filePath) { try { const value = trimTrailingNewline(fs.readFileSync(filePath, "utf8")); return value.length > 0 ? value : undefined; } catch { return undefined; } } /** Best-effort, non-throwing read of a secret-store credential for redaction and health. */ export function lookupApiKeySecretRefValue(ref) { try { return resolveSecret(ref, resolveSecretFromStore); } catch { return undefined; } } function selectedEngineName(config, layers, llmOnly) { for (let index = layers.length - 1; index >= 0; index--) { const engine = layers[index]?.engine; if (engine !== undefined) return engine; } return llmOnly ? config.defaults?.llmEngine : config.defaults?.engine; } /** The configured engine of this name; an unconfigured name is an error. */ export function configuredEngine(name, config) { const engine = config.engines && Object.hasOwn(config.engines, name) ? config.engines[name] : undefined; if (!engine) throw new ConfigError(`Engine "${name}" is not configured.`, "INVALID_CONFIG_FILE"); return engine; } function resolveCredential(name, engine, config) { if (engine.apiKey !== undefined) { const explicit = envName(engine.apiKey); if (explicit) return { names: [explicit], required: true }; // A secret-store reference is carried as `apiKeySecretRef` instead. if (SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey)) return undefined; throw new ConfigError(`Engine "${name}" has an invalid symbolic apiKey reference.`, "INVALID_CONFIG_FILE"); } // An explicit apiKeyFile is its own credential source; it does not also // fall through to the implicit AKM_ENGINE_<NAME>_API_KEY convention. if (engine.apiKeyFile !== undefined) return undefined; const specific = `AKM_ENGINE_${name.toUpperCase().replaceAll("-", "_")}_API_KEY`; return config.defaults?.llmEngine === name ? { names: [specific, "AKM_LLM_API_KEY"], required: false } : { names: [specific], required: false }; } /** Lookup-only credential projection used by redaction inventories. */ export function lookupCredentialFromEnv(credential, envSource = process.env) { for (const name of credential?.names ?? []) { const candidate = envSource[name]?.trim(); if (candidate) return candidate; } return undefined; } /** The enforcing env-credential lookup; a missing required descriptor names its primary variable. */ export function resolveCredentialFromEnv(credential, envSource = process.env) { const value = lookupCredentialFromEnv(credential, envSource); if (value) return value; if (credential?.required) { throw new ConfigError(`Required engine credential ${credential.names[0]} is not set.`, "INVALID_CONFIG_FILE"); } return undefined; } /** * Resolve one LLM credential value at the dispatch boundary: the env * descriptor first, then the file-backed value, then the secret-store * reference. Nothing before dispatch reads any of them. */ export function resolveLlmCredentialValue(engine, credential, apiKeyFile, apiKeySecretRef, envSource = process.env) { const envValue = resolveCredentialFromEnv(credential, envSource); if (envValue !== undefined) return envValue; if (apiKeyFile !== undefined) return readApiKeyFile(engine, apiKeyFile); return apiKeySecretRef !== undefined ? resolveSecret(apiKeySecretRef, resolveSecretFromStore) : undefined; } /** * Non-throwing credential-presence check for `akm health` and improve's * strategy probe. Names WHICH env var / file / secret reference is missing, * never its value; a broken source is reported by the real dispatch. */ export function describeLlmCredentialAvailability(resolved, env = process.env) { if (resolved.credential?.required) { if (resolved.credential.names.some((name) => Boolean(env[name]?.trim()))) return { available: true }; const reference = `$${resolved.credential.names[0]}`; return { available: false, reference, reason: `${reference} is not set in this environment` }; } if (resolved.apiKeyFile !== undefined) { if (lookupApiKeyFileValue(resolved.apiKeyFile) !== undefined) return { available: true }; return { available: false, reference: resolved.apiKeyFile, reason: `apiKeyFile ${resolved.apiKeyFile} is missing or empty`, }; } if (resolved.apiKeySecretRef !== undefined) { if (lookupApiKeySecretRefValue(resolved.apiKeySecretRef) !== undefined) return { available: true }; return { available: false, reference: resolved.apiKeySecretRef, reason: `${resolved.apiKeySecretRef} did not resolve from the secret store`, }; } return { available: true }; } export function isLlmCredentialAvailable(resolved, env = process.env) { return describeLlmCredentialAvailability(resolved, env).available; } /** Collect every configured engine's current credential value for output and persistence redaction. */ export function collectEngineCredentialValues(config, envSource = process.env) { const values = new Set(); for (const [name, engine] of Object.entries(config.engines ?? {})) { if (engine.kind !== "llm") continue; for (const envVar of resolveCredential(name, engine, config)?.names ?? []) { const value = envSource[envVar]?.trim(); if (value) values.add(value); } // File and secret-store lookups are best-effort so one broken engine never // stops redaction from collecting every other engine's value. if (engine.apiKeyFile !== undefined) { const value = lookupApiKeyFileValue(expandHomePath(engine.apiKeyFile)); if (value) values.add(value); } if (engine.apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey)) { const value = lookupApiKeySecretRefValue(engine.apiKey); if (value) values.add(value); } } return collectSensitiveValues(values); } function effectiveTimeout(engine, layers, fallback) { for (let index = layers.length - 1; index >= 0; index--) { const layer = layers[index]; if (layer && Object.hasOwn(layer, "timeoutMs")) return layer.timeoutMs ?? null; } return Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : fallback; } function rawLlmConnection(engine) { const connection = {}; for (const key of LLM_CONNECTION_FIELDS) { if (engine[key] !== undefined) connection[key] = engine[key]; } return connection; } /** Resolve one selected LLM engine and overlays without materializing credentials. */ export function resolveLlmEngineUse(config, layers) { const name = selectedEngineName(config, layers, true); if (!name) { throw new ConfigError("No LLM engine is selected. Set defaults.llmEngine or specify engine.", "LLM_NOT_CONFIGURED"); } const engine = configuredEngine(name, config); if (engine.kind !== "llm") throw new ConfigError(`Engine "${name}" is not an LLM engine.`, "INVALID_CONFIG_FILE"); let connection = rawLlmConnection(engine); for (const layer of layers) { if (layer.llm) connection = deepMergeConfig(connection, layer.llm); if (layer.model !== undefined) connection.model = layer.model; } for (const key of Object.keys(connection)) { if (connection[key] === undefined) delete connection[key]; } const apiKeySecretRef = engine.apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey) ? engine.apiKey : undefined; return { engine: name, connection: connection, credential: resolveCredential(name, engine, config), ...(engine.apiKeyFile !== undefined ? { apiKeyFile: expandHomePath(engine.apiKeyFile) } : {}), ...(apiKeySecretRef !== undefined ? { apiKeySecretRef } : {}), timeoutMs: effectiveTimeout(engine, layers, DEFAULT_LLM_TIMEOUT_MS), }; } /** Inject an already-resolved credential value into a connection; reads nothing itself. */ export function materializeLlmConnectionWithCredential(resolved, credentialValue) { const extraParams = resolved.connection.extraParams; if (extraParams !== undefined) { const issue = validateExtraParams(extraParams)[0]; if (issue) { throw new ConfigError(formatExtraParamsIssue(`Engine "${resolved.engine}" extraParams`, issue), "INVALID_CONFIG_FILE"); } } return { ...resolved.connection, ...(credentialValue ? { apiKey: credentialValue } : {}), timeoutMs: resolved.timeoutMs, }; } /** Read and inject one resolved credential at the runtime boundary. */ export function materializeLlmConnection(resolved, envSource = process.env) { return materializeLlmConnectionWithCredential(resolved, resolveLlmCredentialValue(resolved.engine, resolved.credential, resolved.apiKeyFile, resolved.apiKeySecretRef, envSource)); } function lowerAgentEngine(name, engine, config) { const harness = getHarness(engine.platform); if (!harness?.capabilities.agentDispatch) { throw new ConfigError(`Engine "${name}" names a platform that cannot dispatch agents: ${engine.platform}.`, "INVALID_CONFIG_FILE"); } const platform = harness.id; const sdk = platform === "opencode-sdk"; const builtin = getBuiltinAgentProfile(platform); const profile = { name, platform, personaChannel: sdk ? "native" : (harness.agentBuilder?.personaChannel ?? "prompt"), bin: engine.bin ?? builtin?.bin ?? (sdk ? OPENCODE_SDK_SERVER_BIN : platform), args: engine.args ?? builtin?.args ?? [], stdio: "captured", ...(builtin?.env ? { env: builtin.env } : {}), envPassthrough: builtin?.envPassthrough ?? [], parseOutput: "text", ...(engine.workspace ? { workspace: path.resolve(engine.workspace) } : {}), ...(engine.model ? { model: engine.model } : {}), }; // An engine that sets no timeoutMs leaves it unset, so a caller's own default // (model work's 600 s) can apply; a dispatch with none runs unbounded. const ownTimeout = Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : undefined; if (!sdk) { return { kind: "agent", engine: name, profile, ...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : {}) }; } // The fallback connection is the engine's own `llmEngine` and nothing else. `defaults.llmEngine` // is the default engine for model work, not a connection every SDK engine borrows: with no // `llmEngine`, opencode resolves provider, model and auth from its own configuration. const fallback = engine.llmEngine ? resolveLlmEngineUse(config, [{ engine: engine.llmEngine }]) : undefined; return { kind: "sdk", engine: name, profile, ...(fallback ? { fallbackConnection: fallback.connection, ...(fallback.credential ? { fallbackCredential: fallback.credential } : {}), ...(fallback.apiKeyFile ? { fallbackApiKeyFile: fallback.apiKeyFile } : {}), ...(fallback.apiKeySecretRef ? { fallbackApiKeySecretRef: fallback.apiKeySecretRef } : {}), fallbackTimeoutMs: fallback.timeoutMs, } : {}), ...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : fallback ? { timeoutMs: fallback.timeoutMs } : {}), }; } /** Resolve a configured engine name to its runner: an LLM connection, a spawned agent, or the SDK. */ export function resolveEngine(name, config, engine = configuredEngine(name, config)) { if (engine.kind !== "llm") return lowerAgentEngine(name, engine, config); const resolved = resolveLlmEngineUse(config, [{ engine: name }]); return { kind: "llm", engine: name, connection: resolved.connection, ...(resolved.credential ? { credential: resolved.credential } : {}), ...(resolved.apiKeyFile ? { apiKeyFile: resolved.apiKeyFile } : {}), ...(resolved.apiKeySecretRef ? { apiKeySecretRef: resolved.apiKeySecretRef } : {}), timeoutMs: resolved.timeoutMs, }; }