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
182 lines (181 loc) • 7.94 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 fs from "node:fs";
import path from "node:path";
import dotenv from "dotenv";
import { loadAdapterExecutionSource } from "../../commands/command/execution-source-loader.js";
import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
import { compareCodePoints, isWithin, toPosix } from "../../core/common.js";
import { parseEnvRef } from "../../core/env-secret-ref.js";
import { UsageError } from "../../core/errors.js";
import { deriveInstallations } from "../../indexer/installations.js";
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
import { resolveAssetPath } from "../../sources/resolve.js";
import { isInferredSecretName } from "../../tasks/log-redaction.js";
import { SECRET_TOKEN_RE } from "../exec/environment.js";
import { detectSecretShapedParams } from "../exec/param-secrets.js";
/**
* A step's frozen environment: literal `env:` values, `pass_env` names, then
* one value-free descriptor per `unit.env` ref (owner, key set, secret-token
* names). Values are read at dispatch (`exec/environment.ts`), never frozen.
*/
export function freezeEnvironment(source, exec, context) {
const literals = Object.entries(source.env ?? {}).map(([name, raw]) => {
const value = String(raw);
// A literal is frozen into plan_json; a credential belongs in an env ref, as for task env.
if (isInferredSecretName(name) || detectSecretShapedParams({ [name]: value }).length > 0) {
throw new UsageError(`Workflow step ${source.id} env.${name} is a secret-shaped literal env value. Store credentials in an env asset (unit.env) rather than workflow source.`, "WORKFLOW_SOURCE_INVALID");
}
return Object.freeze({ kind: "literal", name, value });
});
const passThrough = (exec?.passEnv ?? []).map((name) => Object.freeze({ kind: "pass-through", name }));
const seen = new Set();
const envRefs = [];
for (const [precedence, ref] of (source.unit?.env ?? []).entries()) {
if (parseEnvRef(ref).type !== "env") {
throw new UsageError(`Expected an env ref; got ${ref}.`, "WORKFLOW_SOURCE_INVALID");
}
const owned = resolveOwnedAssetSync(ref, "env", context);
if (seen.has(owned.ref))
continue;
seen.add(owned.ref);
const parsed = dotenv.parse(fs.readFileSync(owned.file));
const secretNames = new Set();
for (const value of Object.values(parsed)) {
for (const match of value.matchAll(SECRET_TOKEN_RE))
if (match[1])
secretNames.add(match[1]);
}
envRefs.push(Object.freeze({
kind: "env-ref",
ref: owned.ref,
owner: Object.freeze({
bundle: owned.bundle,
adapter: owned.adapter,
requestedRoot: owned.root,
requestedPath: owned.file,
relativePath: toPosix(path.relative(owned.root, owned.file)),
}),
keys: Object.keys(parsed).sort(compareCodePoints),
secretNames: [...secretNames].sort(compareCodePoints),
precedence,
}));
}
return [...literals, ...passThrough, ...envRefs];
}
/** Load a command or persona the workflow names, resolved through the workflow's own bundles. */
export async function workflowExecutionSource(ref, kind, context) {
const owned = await resolveOwnedAsset(ref, kind === "command" ? "command" : "agent", context);
return kind === "command"
? loadAdapterExecutionSource(owned.ref, "command", { config: context.config })
: loadAdapterExecutionSource(owned.ref, "persona", { config: context.config });
}
export async function resolveOwnedAsset(ref, type, context) {
return resolveOwnedAssetCore(ref, type, context, false);
}
export function resolveOwnedAssetSync(ref, type, context) {
return resolveOwnedAssetCore(ref, type, context, true);
}
function resolveOwnedAssetCore(refInput, type, context, sync) {
const parsed = parseBundleRef(refInput);
const plural = type === "env" ? "env" : `${type}s`;
const conceptId = parsed.conceptId.startsWith(`${plural}/`) ? parsed.conceptId : `${plural}/${parsed.conceptId}`;
const name = conceptId.slice(plural.length + 1);
const direct = parsed.bundle ? configuredOwner(parsed.bundle, context.config) : undefined;
const candidates = direct ? [direct] : installedOwners(parsed.bundle, context.config);
const findSync = () => {
for (const candidate of candidates) {
const directory = path.join(candidate.root, plural);
for (const extension of assetExtensions(type)) {
const file = path.resolve(directory, `${name}${extension}`);
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
if (!isWithin(file, candidate.root)) {
throw new UsageError(`${file} resolves outside its owning root.`, "PATH_ESCAPE_VIOLATION");
}
return { ...candidate, ref: makeBundleRef(candidate.bundle, conceptId), file };
}
}
}
throw new UsageError(`Workflow source target ${refInput} was not found.`, "INVALID_FLAG_VALUE");
};
if (sync)
return findSync();
return (async () => {
for (const candidate of candidates) {
try {
const file = await resolveAssetPath(candidate.root, type, name);
return { ...candidate, ref: makeBundleRef(candidate.bundle, conceptId), file };
}
catch {
// Continue in installation priority order.
}
}
return findSync();
})();
}
/** Every installed bundle root (or just `bundle`'s), in installation priority order. */
function installedOwners(bundle, config) {
const sources = resolveSourceEntries(undefined, config);
const installations = deriveInstallations(sources);
return sources.flatMap((source, index) => {
const installation = installations[index];
if (!installation || (bundle && installation.id !== bundle))
return [];
return [
{
bundle: installation.id,
root: source.path,
adapter: source.adapterId ?? installation.components[0]?.adapter ?? "akm",
},
];
});
}
function configuredOwner(bundle, config) {
const entry = config.bundles?.[bundle];
if (!entry || typeof entry.path !== "string")
return undefined;
const components = entry.components ? Object.values(entry.components) : [];
const component = components[0];
return {
bundle,
root: path.resolve(entry.path, component?.root ?? "."),
adapter: component?.adapter ?? "akm",
};
}
const SCRIPT_EXTENSIONS = [
"",
".sh",
".ts",
".js",
".py",
".rb",
".go",
".pl",
".php",
".lua",
".r",
".swift",
".kt",
".kts",
".ps1",
".cmd",
".bat",
];
function assetExtensions(type) {
if (type === "script")
return SCRIPT_EXTENSIONS;
if (type === "env")
return ["", ".env"];
return ["", ".md", ".yml"];
}
export function qualifyRef(ref, plural, asset, config) {
const parsed = parseBundleRef(ref);
if (parsed.bundle)
return ref;
const bundle = parseBundleRef(asset.ref).bundle ?? config.defaultBundle;
if (!bundle)
throw new UsageError(`Workflow ref ${ref} has no owning bundle.`, "WORKFLOW_SOURCE_INVALID");
const concept = parsed.conceptId.startsWith(`${plural}/`) ? parsed.conceptId : `${plural}/${parsed.conceptId}`;
return makeBundleRef(bundle, concept);
}