akm-cli 0.8.0-rc2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
package/dist/commands/info.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import { getAssetTypes } from "../core/asset-spec";
|
|
3
6
|
import { getSources, loadConfig } from "../core/config";
|
package/dist/commands/init.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* akm initialization logic.
|
|
3
6
|
*
|
|
@@ -9,10 +12,55 @@ import fs from "node:fs";
|
|
|
9
12
|
import path from "node:path";
|
|
10
13
|
import { TYPE_DIRS } from "../core/asset-spec";
|
|
11
14
|
import { loadUserConfig, saveConfig } from "../core/config";
|
|
12
|
-
import {
|
|
15
|
+
import { ConfigError } from "../core/errors";
|
|
16
|
+
import { assertSafeStashDir, getBinDir, getConfigPath, getDefaultStashDir } from "../core/paths";
|
|
13
17
|
import { ensureRg } from "../setup/ripgrep-install";
|
|
18
|
+
/**
|
|
19
|
+
* Refuse to persist a temporary-directory stashDir to the user's config when
|
|
20
|
+
* running under a test runner AND `--dir <tempdir>` was passed explicitly.
|
|
21
|
+
* This guard targets the exact agent-overreach pattern documented in
|
|
22
|
+
* `memory:akm-init-persists-stashdir-warning`: an agent ran
|
|
23
|
+
* `akm init --dir $(mktemp -d)` for an E2E test and silently rewrote the
|
|
24
|
+
* developer's real config to point at a now-deleted temp dir.
|
|
25
|
+
*
|
|
26
|
+
* Tests that legitimately resolve a tempdir via HOME (default-path init) are
|
|
27
|
+
* unaffected — those are normal `~/akm` resolutions and not the failure mode.
|
|
28
|
+
*
|
|
29
|
+
* Test sentinels (either suffices):
|
|
30
|
+
* - `BUN_TEST=1` — explicit opt-in
|
|
31
|
+
* - `NODE_ENV=test` — what `bun test` sets today
|
|
32
|
+
*
|
|
33
|
+
* Tests that genuinely need to exercise `akm init --dir /tmp/...` should set
|
|
34
|
+
* `AKM_FORCE_INIT_TMP_STASH=1`.
|
|
35
|
+
*/
|
|
36
|
+
function assertInitSandbox(stashDir, dirExplicitlyProvided) {
|
|
37
|
+
if (!dirExplicitlyProvided)
|
|
38
|
+
return; // Only guard explicit --dir, not default HOME resolution.
|
|
39
|
+
const isUnderTest = process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
|
|
40
|
+
if (!isUnderTest)
|
|
41
|
+
return;
|
|
42
|
+
if (process.env.AKM_FORCE_INIT_TMP_STASH === "1")
|
|
43
|
+
return;
|
|
44
|
+
const isTmp = stashDir.startsWith("/tmp/") ||
|
|
45
|
+
stashDir === "/tmp" ||
|
|
46
|
+
stashDir.startsWith("/var/tmp/") ||
|
|
47
|
+
stashDir === "/var/tmp" ||
|
|
48
|
+
stashDir.startsWith("/private/var/folders/") ||
|
|
49
|
+
stashDir.startsWith("/private/tmp/");
|
|
50
|
+
if (!isTmp)
|
|
51
|
+
return;
|
|
52
|
+
throw new ConfigError(`refusing to persist --dir stashDir to a temporary path while under test runner; set AKM_FORCE_INIT_TMP_STASH=1 if you really mean it (stashDir=${stashDir})`, "INIT_TMP_STASH_REFUSED");
|
|
53
|
+
}
|
|
14
54
|
export async function akmInit(options) {
|
|
15
55
|
const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
|
|
56
|
+
// Safety check (#473): refuse stashDir at /, $HOME, /etc, ~/.config, etc.
|
|
57
|
+
// Runs BEFORE any disk write — a fat-fingered `akm init --dir /` or
|
|
58
|
+
// `akm init --dir ~` would otherwise mkdir + git-init the user's system
|
|
59
|
+
// root or home directory. Catastrophic-on-misuse vs. trivial-to-recover-from.
|
|
60
|
+
assertSafeStashDir(stashDir);
|
|
61
|
+
// Defense-in-depth: refuse to persist an explicit --dir /tmp/... stashDir
|
|
62
|
+
// to config under a test runner. Default HOME-resolved paths are exempt.
|
|
63
|
+
assertInitSandbox(stashDir, options?.dir != null);
|
|
16
64
|
let created = false;
|
|
17
65
|
if (!fs.existsSync(stashDir)) {
|
|
18
66
|
fs.mkdirSync(stashDir, { recursive: true });
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Source operations: list, remove, update.
|
|
3
6
|
*
|
|
@@ -16,7 +19,6 @@ import { parseGitRepoUrl, syncMirroredRepo } from "../sources/providers/git";
|
|
|
16
19
|
import { syncFromRef } from "../sources/providers/sync-from-ref";
|
|
17
20
|
import { ensureWebsiteMirror } from "../sources/website-ingest";
|
|
18
21
|
import { listWikis, resolveWikisRoot } from "../wiki/wiki";
|
|
19
|
-
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailureForAction, } from "./install-audit";
|
|
20
22
|
import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add";
|
|
21
23
|
import { removeStash } from "./source-manage";
|
|
22
24
|
export async function akmListSources(input) {
|
|
@@ -196,29 +198,11 @@ async function updateWebsiteSource(stashDir, target, all, websiteSource) {
|
|
|
196
198
|
return buildUpdateResponse(stashDir, target, all, []);
|
|
197
199
|
}
|
|
198
200
|
/** Sync a single installed registry entry and return the processed record. */
|
|
199
|
-
async function updateRegistryEntry(entry, force
|
|
201
|
+
async function updateRegistryEntry(entry, force) {
|
|
200
202
|
if (force && shouldCleanupCache(entry)) {
|
|
201
203
|
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
202
204
|
}
|
|
203
205
|
const synced = await syncFromRef(entry.ref, { force });
|
|
204
|
-
// Mirror the post-sync audit hook from akmAdd so `akm update` can't
|
|
205
|
-
// silently land malicious content during refresh.
|
|
206
|
-
const registryLabels = deriveRegistryLabels({
|
|
207
|
-
source: synced.source,
|
|
208
|
-
ref: synced.ref,
|
|
209
|
-
artifactUrl: synced.artifactUrl,
|
|
210
|
-
});
|
|
211
|
-
enforceRegistryInstallPolicy(registryLabels, auditConfig, entry.ref);
|
|
212
|
-
const audit = auditInstallCandidate({
|
|
213
|
-
rootDir: synced.extractedDir,
|
|
214
|
-
source: synced.source,
|
|
215
|
-
ref: synced.ref,
|
|
216
|
-
registryLabels,
|
|
217
|
-
config: auditConfig,
|
|
218
|
-
});
|
|
219
|
-
if (audit.blocked) {
|
|
220
|
-
throw new UsageError(formatInstallAuditFailureForAction(synced.ref, audit, "update"), "INVALID_FLAG_VALUE", `Re-run with \`akm update ${synced.ref} --trust\` only if you intentionally trust this source.`);
|
|
221
|
-
}
|
|
222
206
|
const installedEntry = {
|
|
223
207
|
id: synced.id,
|
|
224
208
|
source: synced.source,
|
|
@@ -255,7 +239,7 @@ async function updateRegistryEntry(entry, force, auditConfig) {
|
|
|
255
239
|
resolvedRevision: entry.resolvedRevision,
|
|
256
240
|
cacheDir: entry.cacheDir,
|
|
257
241
|
},
|
|
258
|
-
installed: { ...installedEntry, extractedDir: synced.extractedDir
|
|
242
|
+
installed: { ...installedEntry, extractedDir: synced.extractedDir },
|
|
259
243
|
changed: {
|
|
260
244
|
version: versionChanged,
|
|
261
245
|
revision: revisionChanged,
|
|
@@ -315,10 +299,9 @@ export async function akmUpdate(input) {
|
|
|
315
299
|
return updateWebsiteSource(stashDir, target, all, websiteMatch);
|
|
316
300
|
}
|
|
317
301
|
const selectedEntries = selectTargets(installedEntries, target, all);
|
|
318
|
-
const auditConfig = config;
|
|
319
302
|
const processed = [];
|
|
320
303
|
for (const entry of selectedEntries) {
|
|
321
|
-
processed.push(await updateRegistryEntry(entry, force
|
|
304
|
+
processed.push(await updateRegistryEntry(entry, force));
|
|
322
305
|
}
|
|
323
306
|
return buildUpdateResponse(stashDir, target, all, processed);
|
|
324
307
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Knowledge-command helpers extracted from `src/cli.ts`.
|
|
3
6
|
*
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import path from "node:path";
|
|
2
5
|
import { BaseLinter } from "./base-linter";
|
|
3
6
|
/**
|
|
@@ -1,5 +1,35 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
// CONTRACT: ref-resolver
|
|
5
|
+
// ----------------------------------------------------------------------------
|
|
6
|
+
// The `refExistsInAnyStash` and `refToRelPath` helpers below are contract-
|
|
7
|
+
// locked: a sister copy lives in the akm-plugins repo at
|
|
8
|
+
// `shared/ref-extraction.ts` (and the runtime-shipped duplicate at
|
|
9
|
+
// `claude/shared/ref-extraction.ts`). Both implementations resolve the same
|
|
10
|
+
// `<type>:<slug>` -> on-disk-asset question and MUST agree on the set of
|
|
11
|
+
// reachable refs for any given stash layout.
|
|
12
|
+
//
|
|
13
|
+
// The lock is enforced by `tests/contracts/ref-resolver-contract.test.ts`,
|
|
14
|
+
// which drives this implementation through a canonical fixture set. The
|
|
15
|
+
// akm-plugins repo ships an equivalent test that drives its copy through the
|
|
16
|
+
// SAME inputs and asserts identical outcomes. Any change to the resolver
|
|
17
|
+
// behavior on either side MUST update both contract tests in lockstep, or one
|
|
18
|
+
// will fail.
|
|
19
|
+
//
|
|
20
|
+
// Cases the contract covers (see fixture in the contract test):
|
|
21
|
+
// - existing memory / knowledge / agent / workflow / skill / vault refs
|
|
22
|
+
// - knowledge subdirectory layout (knowledge/<category>/<slug>.md)
|
|
23
|
+
// - skill multi-file layout (skills/<slug>/SKILL.md)
|
|
24
|
+
// - memory `.derived.md` sibling
|
|
25
|
+
// - vault default vs named (.env vs <name>.env)
|
|
26
|
+
// - namespaced slugs containing `/`
|
|
27
|
+
// - non-existent refs
|
|
28
|
+
// - script type (unresolvable by design — both must return false)
|
|
29
|
+
// ----------------------------------------------------------------------------
|
|
1
30
|
import fs from "node:fs";
|
|
2
31
|
import path from "node:path";
|
|
32
|
+
import { findSafeInsertionPoint } from "./markdown-insertion";
|
|
3
33
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
4
34
|
function formatDate(d) {
|
|
5
35
|
const y = d.getFullYear();
|
|
@@ -56,8 +86,12 @@ function checkStalePath(body) {
|
|
|
56
86
|
}
|
|
57
87
|
// ── missing-ref helpers ───────────────────────────────────────────────────────
|
|
58
88
|
const REF_RE = /(?:^|[\s`"'(])((agent|command|knowledge|memory|script|skill|workflow|lesson|task|wiki|vault):[^\s"'`)\]>,\n]+)/gm;
|
|
59
|
-
/**
|
|
60
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Map from ref type to relative path pattern within stashRoot. Returns null to skip.
|
|
91
|
+
*
|
|
92
|
+
* Exported for contract testing — see header CONTRACT block.
|
|
93
|
+
*/
|
|
94
|
+
export function refToRelPath(refType, refName) {
|
|
61
95
|
switch (refType) {
|
|
62
96
|
case "agent":
|
|
63
97
|
return path.join("agents", `${refName}.md`);
|
|
@@ -80,7 +114,13 @@ function refToRelPath(refType, refName) {
|
|
|
80
114
|
case "wiki":
|
|
81
115
|
return path.join("wikis", `${refName}.md`);
|
|
82
116
|
case "vault":
|
|
83
|
-
|
|
117
|
+
// Vaults are .env files. The canonical name "default" (or empty) maps to
|
|
118
|
+
// ".env"; any other name maps to "<name>.env". This mirrors the vault
|
|
119
|
+
// asset-spec toAssetPath logic in src/core/asset-spec.ts.
|
|
120
|
+
if (!refName || refName === "default") {
|
|
121
|
+
return path.join("vaults", ".env");
|
|
122
|
+
}
|
|
123
|
+
return path.join("vaults", `${refName}.env`);
|
|
84
124
|
default:
|
|
85
125
|
return null;
|
|
86
126
|
}
|
|
@@ -88,8 +128,10 @@ function refToRelPath(refType, refName) {
|
|
|
88
128
|
/**
|
|
89
129
|
* Returns true if `relPath` resolves to a real file (or multi-file directory
|
|
90
130
|
* primary) in ANY of the provided stash roots.
|
|
131
|
+
*
|
|
132
|
+
* Exported for contract testing — see header CONTRACT block.
|
|
91
133
|
*/
|
|
92
|
-
function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
134
|
+
export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
93
135
|
for (const root of stashRoots) {
|
|
94
136
|
const absPath = path.join(root, relPath);
|
|
95
137
|
if (fs.existsSync(absPath))
|
|
@@ -190,6 +232,113 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
|
|
|
190
232
|
}
|
|
191
233
|
return missing;
|
|
192
234
|
}
|
|
235
|
+
// ── frontmatter refs ─────────────────────────────────────────────────────────
|
|
236
|
+
/**
|
|
237
|
+
* Return the `refs:` array from frontmatter when it is present and is an
|
|
238
|
+
* array of strings; otherwise return `null` to signal the caller should
|
|
239
|
+
* fall back to scanning the body. An empty array (`refs: []`) is also
|
|
240
|
+
* treated as authoritative — it explicitly declares "this asset has no
|
|
241
|
+
* outbound refs" and suppresses the body scan.
|
|
242
|
+
*
|
|
243
|
+
* The `refs:` frontmatter key is used by the claude-code session-capture
|
|
244
|
+
* hook (see `shared/ref-extraction.ts` in the akm-plugins repo) to
|
|
245
|
+
* persist a validated outbound-ref list alongside the raw transcript.
|
|
246
|
+
* Hand-written memories rarely populate this key — for those the body
|
|
247
|
+
* scan remains the source of truth.
|
|
248
|
+
*
|
|
249
|
+
* Session-checkpoint memories use a nested frontmatter pattern: `akm
|
|
250
|
+
* remember` wraps the file in `---\n…\n---` and the hook's own
|
|
251
|
+
* `---\nakm_memory_kind: session_checkpoint\n…\n---` block is preserved
|
|
252
|
+
* inside the body. We look in both places so the `refs:` key works
|
|
253
|
+
* regardless of where the producer wrote it.
|
|
254
|
+
*/
|
|
255
|
+
function extractFrontmatterRefs(data, body) {
|
|
256
|
+
const fromOuter = readRefsArray(data.refs);
|
|
257
|
+
if (fromOuter !== null)
|
|
258
|
+
return fromOuter;
|
|
259
|
+
const innerData = parseInnerFrontmatterBlock(body);
|
|
260
|
+
if (innerData) {
|
|
261
|
+
const fromInner = readRefsArray(innerData.refs);
|
|
262
|
+
if (fromInner !== null)
|
|
263
|
+
return fromInner;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function readRefsArray(value) {
|
|
268
|
+
if (!Array.isArray(value))
|
|
269
|
+
return null;
|
|
270
|
+
const out = [];
|
|
271
|
+
for (const entry of value) {
|
|
272
|
+
if (typeof entry === "string" && entry.trim())
|
|
273
|
+
out.push(entry.trim());
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Detect a leading nested frontmatter block in `body` (i.e. a `---\n…\n---`
|
|
279
|
+
* pair that opens within the first few lines of the body). When present,
|
|
280
|
+
* parse a minimal subset of YAML — top-level scalars and block-list
|
|
281
|
+
* arrays — sufficient to recognise the `refs:` key. Anything fancier is
|
|
282
|
+
* silently ignored.
|
|
283
|
+
*
|
|
284
|
+
* This is a deliberately narrow parser: lint must never throw on
|
|
285
|
+
* unexpected YAML, and the only key we care about here is `refs:`.
|
|
286
|
+
*/
|
|
287
|
+
function parseInnerFrontmatterBlock(body) {
|
|
288
|
+
// Skip up to three blank/header lines, then require `---` to open the block.
|
|
289
|
+
const lines = body.split(/\r?\n/);
|
|
290
|
+
let i = 0;
|
|
291
|
+
while (i < lines.length && i < 3 && lines[i].trim() === "")
|
|
292
|
+
i += 1;
|
|
293
|
+
if (lines[i] !== "---")
|
|
294
|
+
return null;
|
|
295
|
+
const open = i;
|
|
296
|
+
let close = -1;
|
|
297
|
+
for (let j = open + 1; j < lines.length; j += 1) {
|
|
298
|
+
if (lines[j] === "---") {
|
|
299
|
+
close = j;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (close === -1)
|
|
304
|
+
return null;
|
|
305
|
+
const block = lines.slice(open + 1, close);
|
|
306
|
+
const data = {};
|
|
307
|
+
let currentKey = null;
|
|
308
|
+
let currentList = null;
|
|
309
|
+
for (const line of block) {
|
|
310
|
+
const listItem = line.match(/^(?: {2})?- (.*)$/);
|
|
311
|
+
if (listItem && currentList) {
|
|
312
|
+
currentList.push(listItem[1].trim().replace(/^["'](.*)["']$/, "$1"));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const inlineFlow = line.match(/^(\w[\w-]*):\s*\[(.*)\]\s*$/);
|
|
316
|
+
if (inlineFlow) {
|
|
317
|
+
currentKey = inlineFlow[1];
|
|
318
|
+
const items = inlineFlow[2]
|
|
319
|
+
.split(",")
|
|
320
|
+
.map((s) => s.trim().replace(/^["'](.*)["']$/, "$1"))
|
|
321
|
+
.filter(Boolean);
|
|
322
|
+
data[currentKey] = items;
|
|
323
|
+
currentList = null;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
327
|
+
if (!kv)
|
|
328
|
+
continue;
|
|
329
|
+
currentKey = kv[1];
|
|
330
|
+
const value = kv[2].trim();
|
|
331
|
+
if (value === "") {
|
|
332
|
+
currentList = [];
|
|
333
|
+
data[currentKey] = currentList;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
data[currentKey] = value.replace(/^["'](.*)["']$/, "$1");
|
|
337
|
+
currentList = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return data;
|
|
341
|
+
}
|
|
193
342
|
// ── BaseLinter ────────────────────────────────────────────────────────────────
|
|
194
343
|
/**
|
|
195
344
|
* Abstract base class providing the two cross-type checks shared by all asset
|
|
@@ -201,6 +350,35 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
|
|
|
201
350
|
* (in practice the base class updates `ctx.raw` in place when `fix` is true).
|
|
202
351
|
*/
|
|
203
352
|
export class BaseLinter {
|
|
353
|
+
/**
|
|
354
|
+
* Insert one or more lines into a markdown body at a safe location.
|
|
355
|
+
*
|
|
356
|
+
* "Safe" means: not inside a markdown table, HTML table, fenced code block,
|
|
357
|
+
* or indented code block. If `proposedLineNumber` falls inside one of those
|
|
358
|
+
* regions, the helper pushes the insertion to immediately after the region.
|
|
359
|
+
* This is a regression guard against the class of bug where an auto-fix
|
|
360
|
+
* splits a table fence by injecting a callout between the separator row
|
|
361
|
+
* and the first data row (broke `knowledge/akm-cli-reference.md` in 0.8.0).
|
|
362
|
+
*
|
|
363
|
+
* Subclasses that perform line-based body insertion MUST route through this
|
|
364
|
+
* helper instead of calling `splice` directly. Insertion fixers must NOT
|
|
365
|
+
* touch frontmatter — use `fixMissingUpdated` / `fixUnquotedColon` style
|
|
366
|
+
* regex edits for that case (those already operate inside the `---…---`
|
|
367
|
+
* fence and don't intersect with body line numbers).
|
|
368
|
+
*
|
|
369
|
+
* @param raw Full file contents (frontmatter + body).
|
|
370
|
+
* @param newLines Lines to insert (without trailing newlines).
|
|
371
|
+
* @param proposedLineNumber 0-based line index within `raw` where the
|
|
372
|
+
* caller wants the new content to appear.
|
|
373
|
+
* @returns The mutated file contents with `newLines` spliced at the
|
|
374
|
+
* adjusted safe position.
|
|
375
|
+
*/
|
|
376
|
+
insertLinesSafely(raw, newLines, proposedLineNumber) {
|
|
377
|
+
const lines = raw.split(/\r?\n/);
|
|
378
|
+
const safeIdx = findSafeInsertionPoint(lines, proposedLineNumber);
|
|
379
|
+
lines.splice(safeIdx, 0, ...newLines);
|
|
380
|
+
return lines.join("\n");
|
|
381
|
+
}
|
|
204
382
|
runBaseChecks(ctx) {
|
|
205
383
|
const issues = [];
|
|
206
384
|
let currentRaw = ctx.raw;
|
|
@@ -271,7 +449,23 @@ export class BaseLinter {
|
|
|
271
449
|
});
|
|
272
450
|
}
|
|
273
451
|
// ── 4. missing-ref ─────────────────────────────────────────────────────
|
|
274
|
-
|
|
452
|
+
// Carve-out for assets that declare an explicit `refs:` array in
|
|
453
|
+
// frontmatter (e.g. session-checkpoint memories captured by the
|
|
454
|
+
// claude-code hook). The frontmatter array is the *authoritative*
|
|
455
|
+
// ref list — any ref-shaped tokens in the body are treated as
|
|
456
|
+
// literal strings (heredocs, grep patterns, JSON values, regex
|
|
457
|
+
// patterns embedded in tool transcripts). Without this carve-out
|
|
458
|
+
// every session capture produces a fresh batch of `missing-ref`
|
|
459
|
+
// flags on every literal `<type>:<slug>` token in a transcript.
|
|
460
|
+
//
|
|
461
|
+
// The producer guarantees that entries in `refs:` already resolve
|
|
462
|
+
// (it validates against the live stash before writing), so we
|
|
463
|
+
// still run `checkMissingRefs` against the array itself to catch
|
|
464
|
+
// refs that were valid at capture time but later removed from the
|
|
465
|
+
// stash.
|
|
466
|
+
const explicitRefs = extractFrontmatterRefs(ctx.data, ctx.body);
|
|
467
|
+
const refSource = explicitRefs !== null ? explicitRefs.join("\n") : ctx.body;
|
|
468
|
+
const missingRefs = checkMissingRefs(refSource, ctx.stashRoot, ctx.extraStashRoots);
|
|
275
469
|
for (const { ref, resolvedRelPath } of missingRefs) {
|
|
276
470
|
issues.push({
|
|
277
471
|
file: ctx.relPath,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import path from "node:path";
|
|
2
5
|
import { BaseLinter } from "./base-linter";
|
|
3
6
|
/**
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import { BaseLinter } from "./base-linter";
|
|
2
5
|
/**
|
|
3
6
|
* Default linter for asset types that have no type-specific rules beyond the
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Vault security lint rules — flags known-dangerous environment variable names.
|
|
6
|
+
*
|
|
7
|
+
* These env var names, when present as vault keys, indicate the vault can be
|
|
8
|
+
* used to hijack process execution via loader injection, path override, or
|
|
9
|
+
* shell/runtime startup hooks. The lint pass emits a warning-level finding;
|
|
10
|
+
* it does NOT block vault load or `akm vault setKey`.
|
|
11
|
+
*
|
|
12
|
+
* Enforcement scope:
|
|
13
|
+
* - `akm lint` reports findings as `dangerous-vault-key` (non-blocking warn).
|
|
14
|
+
* - `akm add` BLOCKS install unless `--allow-insecure` is set (or, on TTY,
|
|
15
|
+
* the user explicitly confirms at the prompt).
|
|
16
|
+
* - `akm vault setKey` does NOT consult this list — by design, the operator
|
|
17
|
+
* owns their own vault and may legitimately store any key locally. The
|
|
18
|
+
* gate exists only for third-party stash installation.
|
|
19
|
+
*
|
|
20
|
+
* False-positive tradeoff:
|
|
21
|
+
* A handful of keys (EDITOR, VISUAL, PAGER) are included because they are
|
|
22
|
+
* invoked by many interactive tools and are a documented RCE vector when
|
|
23
|
+
* sourced from untrusted vaults. They will also flag on benign vaults
|
|
24
|
+
* where the operator legitimately wants to set their editor — accept the
|
|
25
|
+
* FP and bypass with `--allow-insecure` after review.
|
|
26
|
+
*/
|
|
27
|
+
import { listKeys } from "../env";
|
|
28
|
+
// ── Dangerous key set ─────────────────────────────────────────────────────────
|
|
29
|
+
export const DANGEROUS_VAULT_KEYS = new Set([
|
|
30
|
+
// Dynamic linker hijacking (Linux glibc ld.so)
|
|
31
|
+
"LD_PRELOAD", // forces shared library injection
|
|
32
|
+
"LD_LIBRARY_PATH", // overrides library search path
|
|
33
|
+
"LD_AUDIT", // loads auditing libs (CVE-class injection vector)
|
|
34
|
+
"LD_DEBUG", // info disclosure / loader behaviour leak
|
|
35
|
+
"LD_BIND_NOW", // eager symbol resolution — can trigger malicious libs
|
|
36
|
+
"LD_PROFILE", // writes profile data — abusable for info disclosure
|
|
37
|
+
"LD_ASSUME_KERNEL", // kernel-version spoofing affecting loader behaviour
|
|
38
|
+
"LD_TRACE_LOADED_OBJECTS", // info disclosure (lists linked libs)
|
|
39
|
+
// Dynamic linker hijacking (macOS dyld)
|
|
40
|
+
"DYLD_INSERT_LIBRARIES", // macOS analogue of LD_PRELOAD
|
|
41
|
+
"DYLD_LIBRARY_PATH", // overrides dyld library search path
|
|
42
|
+
"DYLD_FRAMEWORK_PATH", // overrides framework search path
|
|
43
|
+
// Shell and command resolution
|
|
44
|
+
"PATH", // command lookup hijack
|
|
45
|
+
"BASH_ENV", // sourced on non-interactive bash startup (RCE)
|
|
46
|
+
"ENV", // sourced on POSIX sh startup (RCE)
|
|
47
|
+
"PROMPT_COMMAND", // command run before each bash prompt (RCE)
|
|
48
|
+
"PS1", // prompt — command substitution arbitrary code
|
|
49
|
+
"PS2", // continuation prompt — command substitution
|
|
50
|
+
"IFS", // Internal Field Separator — classic word-splitting attack
|
|
51
|
+
// Shell startup hijack
|
|
52
|
+
"ZDOTDIR", // zsh startup file lookup directory hijack
|
|
53
|
+
// Language runtime hijacking — Node.js
|
|
54
|
+
"NODE_OPTIONS", // injects flags incl. --require module-load RCE
|
|
55
|
+
"NODE_PATH", // module resolution hijack
|
|
56
|
+
"NODE_TLS_REJECT_UNAUTHORIZED", // silently disables TLS verification — MITM enabler
|
|
57
|
+
// Language runtime hijacking — Python
|
|
58
|
+
"PYTHONSTARTUP", // sourced by interactive python (RCE)
|
|
59
|
+
"PYTHONPATH", // module resolution hijack
|
|
60
|
+
"PYTHONINSPECT", // drops into REPL after script — sandbox escape vector
|
|
61
|
+
"PYTHONHOME", // python install prefix hijack
|
|
62
|
+
"PYTHONNOUSERSITE", // disables user-site isolation — sandbox weakening
|
|
63
|
+
// Language runtime hijacking — Ruby
|
|
64
|
+
"RUBYLIB", // ruby load path hijack
|
|
65
|
+
"RUBYOPT", // injects ruby command-line opts
|
|
66
|
+
// Language runtime hijacking — Perl
|
|
67
|
+
"PERL5LIB", // perl @INC hijack
|
|
68
|
+
"PERL5OPT", // injects perl command-line opts
|
|
69
|
+
// Language runtime hijacking — Java
|
|
70
|
+
"JAVA_TOOL_OPTIONS", // honoured by every JVM — flag injection / agent load
|
|
71
|
+
"JDK_JAVA_OPTIONS", // JDK launcher options injection
|
|
72
|
+
"_JAVA_OPTIONS", // legacy JVM options injection
|
|
73
|
+
// Git (RCE via git invocations)
|
|
74
|
+
"GIT_SSH_COMMAND", // replaces ssh with arbitrary command (RCE)
|
|
75
|
+
"GIT_EXTERNAL_DIFF", // runs arbitrary command during diff (RCE)
|
|
76
|
+
"GIT_PAGER", // runs arbitrary command for paging (RCE)
|
|
77
|
+
"GIT_EDITOR", // runs arbitrary command for editor (RCE)
|
|
78
|
+
// Interactive-tool invocation hijack — high FP rate but documented RCE vectors
|
|
79
|
+
"EDITOR", // invoked by git, crontab, sudoedit, etc. (RCE)
|
|
80
|
+
"VISUAL", // EDITOR fallback used by many tools (RCE)
|
|
81
|
+
"PAGER", // invoked by git, man, systemctl, etc. (RCE)
|
|
82
|
+
]);
|
|
83
|
+
/**
|
|
84
|
+
* Pattern-based dangerous key matchers.
|
|
85
|
+
*
|
|
86
|
+
* Some attack vectors target a family of variable names rather than a single
|
|
87
|
+
* literal — most famously Shellshock (CVE-2014-6271), which exploits keys
|
|
88
|
+
* prefixed with `BASH_FUNC_`. Listing every concrete name is impossible; we
|
|
89
|
+
* test against this pattern set in addition to the literal `Set`.
|
|
90
|
+
*/
|
|
91
|
+
export const DANGEROUS_VAULT_KEY_PATTERNS = [
|
|
92
|
+
{
|
|
93
|
+
// CVE-2014-6271 (Shellshock) — bash imports exported functions named
|
|
94
|
+
// `BASH_FUNC_<name>%%` and parses their bodies, enabling RCE.
|
|
95
|
+
pattern: /^BASH_FUNC_/,
|
|
96
|
+
reason: "Shellshock-class bash function injection (CVE-2014-6271)",
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* Returns `true` if the given key name is dangerous — either by literal match
|
|
101
|
+
* against `DANGEROUS_VAULT_KEYS` or by matching any entry in
|
|
102
|
+
* `DANGEROUS_VAULT_KEY_PATTERNS`.
|
|
103
|
+
*/
|
|
104
|
+
export function isDangerousVaultKey(key) {
|
|
105
|
+
if (DANGEROUS_VAULT_KEYS.has(key))
|
|
106
|
+
return true;
|
|
107
|
+
for (const { pattern } of DANGEROUS_VAULT_KEY_PATTERNS) {
|
|
108
|
+
if (pattern.test(key))
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
// ── Checker ───────────────────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Inspect a vault `.env` file and return a lint finding for every key whose
|
|
116
|
+
* name appears in `DANGEROUS_VAULT_KEYS` or matches a pattern in
|
|
117
|
+
* `DANGEROUS_VAULT_KEY_PATTERNS`.
|
|
118
|
+
*
|
|
119
|
+
* @param vaultPath Absolute path to the `.env` file.
|
|
120
|
+
* @param relPath Stash-relative path used as the `file` field in findings
|
|
121
|
+
* (e.g. `"vaults/prod.env"`).
|
|
122
|
+
* @param vaultRef Human-readable vault ref (e.g. `"vault:prod"`) shown in
|
|
123
|
+
* the finding message.
|
|
124
|
+
*/
|
|
125
|
+
export function checkVaultForDangerousKeys(vaultPath, relPath, vaultRef) {
|
|
126
|
+
const { keys } = listKeys(vaultPath);
|
|
127
|
+
const issues = [];
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
if (!isDangerousVaultKey(key))
|
|
130
|
+
continue;
|
|
131
|
+
issues.push({
|
|
132
|
+
file: relPath,
|
|
133
|
+
issue: "dangerous-vault-key",
|
|
134
|
+
detail: `Env key \`${key}\` can be used to hijack process execution when injected via \`akm env run\`. Ref: ${vaultRef}. Review this file before running \`akm env run\` commands against untrusted stashes.`,
|
|
135
|
+
fixed: false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return issues;
|
|
139
|
+
}
|
|
140
|
+
// ── Env-neutral aliases ───────────────────────────────────────────────────────
|
|
141
|
+
//
|
|
142
|
+
// These primitives guard *environment variable injection*, which is shared by
|
|
143
|
+
// the `env` asset type, whole-file `secret` injection, and the `akm add`
|
|
144
|
+
// supply-chain gate. The original `*Vault*` names are retained above for
|
|
145
|
+
// backward compatibility (and stable lint output) through the 0.8.x
|
|
146
|
+
// deprecation window; new call sites should prefer the env-neutral names.
|
|
147
|
+
/** Env-neutral alias of {@link DANGEROUS_VAULT_KEYS}. */
|
|
148
|
+
export const DANGEROUS_ENV_KEYS = DANGEROUS_VAULT_KEYS;
|
|
149
|
+
/** Env-neutral alias of {@link DANGEROUS_VAULT_KEY_PATTERNS}. */
|
|
150
|
+
export const DANGEROUS_ENV_KEY_PATTERNS = DANGEROUS_VAULT_KEY_PATTERNS;
|
|
151
|
+
/** Env-neutral alias of {@link isDangerousVaultKey}. */
|
|
152
|
+
export const isDangerousEnvKey = isDangerousVaultKey;
|
|
153
|
+
/** Env-neutral alias of {@link checkVaultForDangerousKeys}. */
|
|
154
|
+
export const checkEnvForDangerousKeys = checkVaultForDangerousKeys;
|