akm-cli 0.8.0-rc1 → 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.
Files changed (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2162 -1258
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +233 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +17 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +662 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +114 -48
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -307
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -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";
@@ -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 { getBinDir, getConfigPath, getDefaultStashDir } from "../core/paths";
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, auditConfig) {
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, audit },
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, auditConfig));
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
- /** Map from ref type to relative path pattern within stashRoot. Returns null to skip. */
60
- function refToRelPath(refType, refName) {
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
- return path.join("vaults", `${refName}.md`);
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))
@@ -104,6 +146,23 @@ function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
104
146
  if (fs.existsSync(derivedPath))
105
147
  return true;
106
148
  }
149
+ // Knowledge-specific: search subdirectories like knowledge/projects/, knowledge/tools/, etc.
150
+ if (refType === "knowledge") {
151
+ try {
152
+ const knowledgeDir = path.join(root, "knowledge");
153
+ if (fs.existsSync(knowledgeDir) && fs.statSync(knowledgeDir).isDirectory()) {
154
+ const entries = fs.readdirSync(knowledgeDir);
155
+ for (const entry of entries) {
156
+ const subPath = path.join(knowledgeDir, entry, `${refName}.md`);
157
+ if (fs.existsSync(subPath))
158
+ return true;
159
+ }
160
+ }
161
+ }
162
+ catch {
163
+ // Ignore errors reading directory
164
+ }
165
+ }
107
166
  // Fallback: the refName may already encode the full stash-relative path
108
167
  // (e.g. knowledge:skills/foo/references/bar where the file lives at
109
168
  // <stash>/skills/foo/references/bar.md, not <stash>/knowledge/skills/...).
@@ -119,6 +178,11 @@ function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
119
178
  /**
120
179
  * Returns an array of {ref, resolvedRelPath} for every local AKM ref in the
121
180
  * body that does not resolve to a real file under any of the provided stash roots.
181
+ *
182
+ * Skips false-positive patterns:
183
+ * - Shell variables: memory:$(cmd) or knowledge:${VAR}
184
+ * - ACP type notation: agent::Type (double colons are C++/ACP syntax)
185
+ * - Incomplete/placeholder refs: slug is single character or "**"
122
186
  */
123
187
  function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
124
188
  const allRoots = [stashRoot, ...extraStashRoots];
@@ -128,6 +192,14 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
128
192
  // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
129
193
  while ((match = re.exec(body)) !== null) {
130
194
  const fullRef = match[1]; // e.g. "workflow:foo" or "local//workflow:foo"
195
+ // Skip shell variables: memory:$(cmd) or knowledge:${VAR}
196
+ if (fullRef.includes("$(") || fullRef.includes("${")) {
197
+ continue;
198
+ }
199
+ // Skip ACP type notation: agent::Type (double colons)
200
+ if (fullRef.includes("::")) {
201
+ continue;
202
+ }
131
203
  // Strip leading "local//" prefix if present
132
204
  let ref = fullRef;
133
205
  if (ref.startsWith("local//")) {
@@ -147,6 +219,10 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
147
219
  if (!refName || refName.startsWith("/") || refName.startsWith("~") || refName.startsWith("http")) {
148
220
  continue;
149
221
  }
222
+ // Skip placeholder/incomplete refs: single character slug or "**"
223
+ if (refName.length <= 1 || refName === "**") {
224
+ continue;
225
+ }
150
226
  const relPath = refToRelPath(refType, refName);
151
227
  if (relPath === null)
152
228
  continue; // type is skipped
@@ -156,6 +232,113 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
156
232
  }
157
233
  return missing;
158
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
+ }
159
342
  // ── BaseLinter ────────────────────────────────────────────────────────────────
160
343
  /**
161
344
  * Abstract base class providing the two cross-type checks shared by all asset
@@ -167,6 +350,35 @@ function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
167
350
  * (in practice the base class updates `ctx.raw` in place when `fix` is true).
168
351
  */
169
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
+ }
170
382
  runBaseChecks(ctx) {
171
383
  const issues = [];
172
384
  let currentRaw = ctx.raw;
@@ -237,7 +449,23 @@ export class BaseLinter {
237
449
  });
238
450
  }
239
451
  // ── 4. missing-ref ─────────────────────────────────────────────────────
240
- const missingRefs = checkMissingRefs(ctx.body, ctx.stashRoot, ctx.extraStashRoots);
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);
241
469
  for (const { ref, resolvedRelPath } of missingRefs) {
242
470
  issues.push({
243
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;