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

145 lines (144 loc) • 7.76 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 { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js"; import { COMPOSITION_INVALID_MULTI_JOB_HINT, ConfigError, NotFoundError, UsageError } from "../../core/errors.js"; import { DURATION_UNITS, parseDuration } from "../../core/time.js"; import { isPortableExecutionAgentSelector } from "../../execution/source.js"; import { buildExecution } from "../../integrations/agent/execution.js"; import { resolveAssetPath } from "../../sources/resolve.js"; import { checkWorkflowPlan, compileWorkflowSource } from "../../workflows/compile.js"; import { detectSecretShapedParams } from "../../workflows/exec/param-secrets.js"; import { isInferredSecretName } from "../log-redaction.js"; import { SCHEDULED_TASK_CONTEXT_KEYS } from "../scheduler-invocation.js"; function own(value, key) { return value !== undefined && Object.hasOwn(value, key); } export function environmentSnapshot(environment) { const out = Object.create(null); for (const key of Object.keys(environment ?? {}).sort()) { const raw = environment?.[key]; if (raw === undefined) continue; const value = String(raw); if (isInferredSecretName(key) || detectSecretShapedParams({ [key]: value }).length > 0) { throw new UsageError(`Task env.${key} is a secret-shaped literal env value. Store credentials in a secret/env binding rather than durable task source.`, "TASK_SOURCE_INVALID"); } Object.defineProperty(out, key, { value, enumerable: true, configurable: false, writable: false }); } return Object.freeze(out); } /** Merge source env with the authoritative, closed scheduler directory context. */ export function commandEnvironmentSnapshot(environment, schedulerContext) { const out = Object.create(null); for (const key of Object.keys(environment)) { Object.defineProperty(out, key, { value: environment[key], enumerable: true, configurable: true, writable: true }); } for (const key of SCHEDULED_TASK_CONTEXT_KEYS) { const value = schedulerContext?.[key]; if (!value) continue; Object.defineProperty(out, key, { value, enumerable: true, configurable: true, writable: true }); } return Object.freeze(out); } function normalizeTimeout(value) { if (value === undefined || value === null || typeof value === "number") return value; const parsed = parseDuration(value, DURATION_UNITS); if (parsed === null) throw new UsageError(`Invalid task timeout ${JSON.stringify(value)}.`, "TASK_SOURCE_INVALID"); return parsed; } export function qualifyOwnedRef(ref, context) { const parsed = parseBundleRef(ref); const bundle = parsed.bundle ?? context.bundleName; return { parsed, qualified: makeBundleRef(bundle, parsed.conceptId) }; } export function currentExecutionValues(document, context, environment) { const akm = document.akm; const agent = akm?.agent; return Object.freeze({ ...(own(akm, "agent") ? { agent: typeof agent === "string" && isPortableExecutionAgentSelector(agent) ? qualifyOwnedRef(agent, context).qualified : agent, } : {}), ...(own(akm, "engine") ? { engine: akm?.engine } : {}), ...(own(akm, "model") ? { model: akm?.model } : {}), ...(own(akm, "inference") ? { inference: akm?.inference } : {}), ...(own(akm, "outputSchema") ? { outputSchema: akm?.outputSchema } : {}), ...(own(akm, "tools") ? { tools: akm?.tools } : {}), ...(own(akm, "timeout") ? { timeout: akm?.timeout } : {}), workspace: context.bundleRoot, environment, }); } export function base(document, context, environment) { const timeoutMs = normalizeTimeout(document.akm?.timeout); return Object.freeze({ taskId: context.taskId, taskRef: context.taskRef, environment, ...(timeoutMs !== undefined ? { timeoutMs } : {}), redact: Object.freeze([...(document.akm?.redact ?? [])]), }); } export async function resolvedOwnedAsset(qualified, type, context) { const parsed = parseBundleRef(qualified); const prefix = `${type}s/`; const name = parsed.conceptId.slice(prefix.length); if (context.resolveAsset) { const resolved = await context.resolveAsset({ bundle: parsed.bundle, type, name, ref: qualified }); return typeof resolved === "string" ? Object.freeze({ file: resolved, bundleRoot: context.bundleRoot }) : Object.freeze({ file: resolved.file, bundleRoot: resolved.bundleRoot }); } if (parsed.bundle !== context.bundleName) { throw new NotFoundError(`Task target ${JSON.stringify(qualified)} names bundle ${JSON.stringify(parsed.bundle)}, but no bundle resolver was provided.`, "ASSET_NOT_FOUND"); } return Object.freeze({ file: await resolveAssetPath(context.bundleRoot, type, name), bundleRoot: context.bundleRoot, }); } export function defaultTaskShell(platform) { return platform === "win32" ? "powershell" : "sh"; } export function validatePreparedCommand(invocation, context) { const { request } = invocation; if (!request.engine.name) { throw new ConfigError(`Task ${JSON.stringify(context.taskRef)} has no resolved execution engine. Configure defaults.engine or akm.engine before running it.`, "INVALID_CONFIG_FILE"); } // Lowering is pure. Running it before the durable-attempt boundary proves // the selected target is transport-projectable; dispatch repeats the same // deterministic projection from this frozen request/config snapshot. buildExecution(request, invocation.runner); return invocation; } export function validateWorkflowRuntimeSource(file, workspaceRoot, readFile) { const source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFile(file, workspaceRoot)); const compiled = compileWorkflowSource(source, { path: file, workspaceRoot }); if (!compiled.ok) { // P4-N2's mapping (docs/plans/specs/p4-deletions-closeout.md §3.3.4): a // task-wrapped workflow target's own multi-job source is a composition // failure, same as freezing it directly would be; any other compile // failure is WORKFLOW_SOURCE_INVALID. const detail = compiled.errors.map((error) => `${error.path}:${error.line}: ${error.message}`).join("; "); const isMultiJob = compiled.errors.length === 1 && compiled.errors[0]?.code === "multi-job-unsupported"; const code = isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID"; throw new UsageError(`Task workflow target is not projectable: ${detail}`, code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined); } const planned = checkWorkflowPlan(compiled.plan); if (!planned.ok) { // Same mapping applied for consistency; `compiled.ir` is already // guaranteed exactly one job here, so this arm always resolves to // WORKFLOW_SOURCE_INVALID in practice. const detail = planned.errors.map((error) => `${file}:${error.line}: ${error.message}`).join("; "); const isMultiJob = planned.errors.length === 1 && planned.errors[0]?.code === "multi-job-unsupported"; const code = isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID"; throw new UsageError(`Task workflow target is not projectable: ${detail}`, code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined); } }