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
JavaScript
// 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);
}
}