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

103 lines (102 loc) • 5.09 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/. /** * Secret asset type — whole-file secret storage. * * A `secret` holds a single SENSITIVE value used on its own for authentication * (a PEM private key, an API token, a TLS cert, a service-account JSON): the * ENTIRE file is the secret. There is no safe region to parse, so only the * filename is ever surfaced. Where an `env` file holds a GROUP of related * configuration and exposes key NAMES as metadata, a secret is ONE value and * exposes nothing but its name — reach for `secret` when one value *is* the * credential, and for `env` when loading a service's related configuration. * * Invariant: a secret's bytes must never be written to stdout, returned * through the indexer / `akm show` renderer, or any structured output channel. * The supported value-use path is: * * - `akm secret run <ref> <VAR> -- <cmd>` — value injected into the child * process env as `VAR=<value>` (see `readValue`). * * `akm secret path <ref>` (print the file path for the Docker `/run/secrets` + * `_FILE` convention) was removed in 0.9.0 alongside `akm secret remove` — an * audit found the two resolved a ref through different stash-selection logic * and could silently target different files. A ref's file still lives at * `<stash>/secrets/<name>`; locate or delete it there directly. * * Values are stored as raw bytes (no quoting, multi-line allowed) so they * round-trip byte-exact, unlike env values which forbid literal newlines. */ import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { safeRealpath, writeFileAtomic } from "../../core/common.js"; import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js"; import { getDataDir } from "../../core/paths.js"; import { sleepSync } from "../../runtime.js"; // ── Write-lock helper ───────────────────────────────────────────────────────── /** * Acquire an exclusive lock for the given secret path, run `fn`, then release. * Mirrors the env write-lock: O_EXCL creation, 5s deadline, PID-based stale * detection. A timeout is always a stale lock or a programming error, so we * throw rather than silently proceeding. */ export function withSecretLock(secretPath, fn) { const lockPath = secretLockPath(secretPath); const lockDir = path.dirname(lockPath); fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 }); const deadline = Date.now() + 5000; let ownership; while (!ownership) { ownership = tryAcquireLockSync(lockPath, createLockPayload()); if (ownership) break; const probe = probeLock(lockPath); if (probe.state === "stale" && reclaimStaleLock(lockPath, probe)) { continue; } if (Date.now() > deadline) { const holderHint = probe.state === "held" ? ` It is held by live PID ${probe.holderPid}.` : ""; throw new Error(`Could not acquire the secret write lock after 5s.${holderHint} Retry once any other akm secret operation finishes.`); } sleepSync(10); } try { return fn(); } finally { releaseLock(ownership); } } export function secretLockPath(secretPath, platform = process.platform) { const canonical = safeRealpath(secretPath); const identity = platform === "win32" ? canonical.toLowerCase() : canonical; const key = createHash("sha256").update(identity).digest("hex"); return path.join(getDataDir(), "secret-locks", `${key}.lock`); } // ── Atomic byte write ────────────────────────────────────────────────────────── function ensureParentDir(filePath) { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } // ── Public API ────────────────────────────────────────────────────────────── /** * Read a secret's raw bytes. Internal use only (for `secret run`). Callers * MUST NOT write the returned value to stdout or any log. */ export function readValue(secretPath) { return fs.readFileSync(secretPath); } /** * Write (create or overwrite) a secret with the given raw bytes, atomically at * mode 0600 under a write-lock. No quoting; multi-line / binary allowed. */ export function setSecret(secretPath, value) { withSecretLock(secretPath, () => { ensureParentDir(secretPath); // Mode 0600: secrets must never be world-readable, even transiently. writeFileAtomic(secretPath, value, 0o600); }); }