akm-cli 0.8.0-rc2 → 0.8.1

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 (313) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +238 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/assets/help/help-accept.md +12 -0
  5. package/dist/assets/help/help-improve.md +81 -0
  6. package/dist/{commands → assets}/help/help-proposals.md +7 -4
  7. package/dist/assets/help/help-reject.md +11 -0
  8. package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
  9. package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
  10. package/dist/assets/profiles/default.json +15 -0
  11. package/dist/assets/profiles/graph-refresh.json +13 -0
  12. package/dist/assets/profiles/memory-focus.json +12 -0
  13. package/dist/assets/profiles/quick.json +15 -0
  14. package/dist/assets/profiles/thorough.json +15 -0
  15. package/dist/assets/prompts/extract-session.md +80 -0
  16. package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
  17. package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
  18. package/dist/cli/config-migrate.js +144 -0
  19. package/dist/cli/config-validate.js +39 -0
  20. package/dist/cli/confirm.js +73 -0
  21. package/dist/cli/parse-args.js +93 -3
  22. package/dist/cli/shared.js +129 -0
  23. package/dist/cli.js +2141 -1268
  24. package/dist/commands/add-cli.js +279 -0
  25. package/dist/commands/agent-dispatch.js +20 -12
  26. package/dist/commands/agent-support.js +11 -5
  27. package/dist/commands/completions.js +3 -0
  28. package/dist/commands/config-cli.js +129 -517
  29. package/dist/commands/consolidate.js +1557 -147
  30. package/dist/commands/curate.js +44 -3
  31. package/dist/commands/db-cli.js +23 -0
  32. package/dist/commands/distill-promotion-policy.js +5 -3
  33. package/dist/commands/distill.js +906 -100
  34. package/dist/commands/env.js +213 -0
  35. package/dist/commands/eval-cases.js +3 -0
  36. package/dist/commands/events.js +3 -0
  37. package/dist/commands/extract-cli.js +127 -0
  38. package/dist/commands/extract-prompt.js +217 -0
  39. package/dist/commands/extract.js +477 -0
  40. package/dist/commands/feedback-cli.js +331 -0
  41. package/dist/commands/graph.js +260 -5
  42. package/dist/commands/health.js +1042 -55
  43. package/dist/commands/history.js +51 -16
  44. package/dist/commands/improve-auto-accept.js +97 -0
  45. package/dist/commands/improve-cli.js +236 -0
  46. package/dist/commands/improve-profiles.js +138 -0
  47. package/dist/commands/improve-result-file.js +167 -0
  48. package/dist/commands/improve.js +1736 -346
  49. package/dist/commands/info.js +26 -28
  50. package/dist/commands/init.js +49 -1
  51. package/dist/commands/installed-stashes.js +6 -23
  52. package/dist/commands/knowledge.js +3 -0
  53. package/dist/commands/lint/agent-linter.js +3 -0
  54. package/dist/commands/lint/base-linter.js +199 -5
  55. package/dist/commands/lint/command-linter.js +3 -0
  56. package/dist/commands/lint/default-linter.js +3 -0
  57. package/dist/commands/lint/env-key-rules.js +154 -0
  58. package/dist/commands/lint/index.js +92 -3
  59. package/dist/commands/lint/knowledge-linter.js +3 -0
  60. package/dist/commands/lint/markdown-insertion.js +343 -0
  61. package/dist/commands/lint/memory-linter.js +3 -0
  62. package/dist/commands/lint/registry.js +3 -0
  63. package/dist/commands/lint/skill-linter.js +3 -0
  64. package/dist/commands/lint/task-linter.js +15 -12
  65. package/dist/commands/lint/types.js +3 -0
  66. package/dist/commands/lint/workflow-linter.js +3 -0
  67. package/dist/commands/lint.js +3 -0
  68. package/dist/commands/migration-help.js +5 -2
  69. package/dist/commands/proposal-drain-policies.js +128 -0
  70. package/dist/commands/proposal-drain.js +477 -0
  71. package/dist/commands/proposal.js +60 -6
  72. package/dist/commands/propose.js +24 -19
  73. package/dist/commands/reflect.js +1004 -94
  74. package/dist/commands/registry-cli.js +150 -0
  75. package/dist/commands/registry-search.js +3 -0
  76. package/dist/commands/remember-cli.js +257 -0
  77. package/dist/commands/remember.js +15 -6
  78. package/dist/commands/schema-repair.js +88 -15
  79. package/dist/commands/search.js +99 -14
  80. package/dist/commands/secret.js +173 -0
  81. package/dist/commands/self-update.js +3 -0
  82. package/dist/commands/show.js +32 -13
  83. package/dist/commands/source-add.js +7 -35
  84. package/dist/commands/source-clone.js +3 -0
  85. package/dist/commands/source-manage.js +3 -0
  86. package/dist/commands/tasks.js +161 -95
  87. package/dist/commands/url-checker.js +3 -0
  88. package/dist/core/action-contributors.js +3 -0
  89. package/dist/core/asset-ref.js +13 -2
  90. package/dist/core/asset-registry.js +9 -2
  91. package/dist/core/asset-serialize.js +88 -0
  92. package/dist/core/asset-spec.js +61 -5
  93. package/dist/core/common.js +93 -5
  94. package/dist/core/concurrent.js +3 -0
  95. package/dist/core/config-io.js +347 -0
  96. package/dist/core/config-migration.js +622 -0
  97. package/dist/core/config-schema.js +558 -0
  98. package/dist/core/config-sources.js +108 -0
  99. package/dist/core/config-types.js +4 -0
  100. package/dist/core/config-walker.js +337 -0
  101. package/dist/core/config.js +366 -1077
  102. package/dist/core/errors.js +42 -20
  103. package/dist/core/events.js +31 -25
  104. package/dist/core/file-lock.js +104 -0
  105. package/dist/core/frontmatter.js +75 -10
  106. package/dist/core/lesson-lint.js +3 -0
  107. package/dist/core/markdown.js +3 -0
  108. package/dist/core/memory-belief.js +62 -0
  109. package/dist/core/memory-contradiction-detect.js +274 -0
  110. package/dist/core/memory-improve.js +142 -14
  111. package/dist/core/parse.js +3 -0
  112. package/dist/core/paths.js +218 -50
  113. package/dist/core/proposal-quality-validators.js +380 -0
  114. package/dist/core/proposal-validators.js +11 -3
  115. package/dist/core/proposals.js +464 -5
  116. package/dist/core/state-db.js +349 -56
  117. package/dist/core/text-truncation.js +107 -0
  118. package/dist/core/time.js +3 -0
  119. package/dist/core/tty.js +59 -0
  120. package/dist/core/warn.js +7 -2
  121. package/dist/core/write-source.js +12 -0
  122. package/dist/indexer/db-backup.js +391 -0
  123. package/dist/indexer/db-search.js +136 -28
  124. package/dist/indexer/db.js +661 -166
  125. package/dist/indexer/ensure-index.js +3 -0
  126. package/dist/indexer/file-context.js +3 -0
  127. package/dist/indexer/graph-boost.js +162 -40
  128. package/dist/indexer/graph-db.js +241 -51
  129. package/dist/indexer/graph-dedup.js +3 -7
  130. package/dist/indexer/graph-extraction.js +242 -149
  131. package/dist/indexer/index-context.js +3 -9
  132. package/dist/indexer/indexer.js +86 -16
  133. package/dist/indexer/llm-cache.js +24 -19
  134. package/dist/indexer/manifest.js +3 -0
  135. package/dist/indexer/matchers.js +184 -11
  136. package/dist/indexer/memory-inference.js +94 -50
  137. package/dist/indexer/metadata-contributors.js +3 -0
  138. package/dist/indexer/metadata.js +110 -50
  139. package/dist/indexer/path-resolver.js +3 -0
  140. package/dist/indexer/project-context.js +192 -0
  141. package/dist/indexer/ranking-contributors.js +134 -7
  142. package/dist/indexer/ranking.js +8 -1
  143. package/dist/indexer/search-fields.js +5 -9
  144. package/dist/indexer/search-hit-enrichers.js +91 -2
  145. package/dist/indexer/search-source.js +20 -1
  146. package/dist/indexer/semantic-status.js +4 -1
  147. package/dist/indexer/staleness-detect.js +447 -0
  148. package/dist/indexer/usage-events.js +12 -9
  149. package/dist/indexer/walker.js +3 -0
  150. package/dist/integrations/agent/builders.js +135 -0
  151. package/dist/integrations/agent/config.js +121 -401
  152. package/dist/integrations/agent/detect.js +3 -0
  153. package/dist/integrations/agent/index.js +6 -14
  154. package/dist/integrations/agent/model-aliases.js +55 -0
  155. package/dist/integrations/agent/profiles.js +3 -0
  156. package/dist/integrations/agent/prompts.js +137 -8
  157. package/dist/integrations/agent/runner.js +208 -0
  158. package/dist/integrations/agent/sdk-runner.js +8 -2
  159. package/dist/integrations/agent/spawn.js +54 -14
  160. package/dist/integrations/github.js +3 -0
  161. package/dist/integrations/lockfile.js +22 -51
  162. package/dist/integrations/session-logs/index.js +4 -0
  163. package/dist/integrations/session-logs/inline-refs.js +35 -0
  164. package/dist/integrations/session-logs/pre-filter.js +152 -0
  165. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  166. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  167. package/dist/integrations/session-logs/types.js +3 -0
  168. package/dist/llm/call-ai.js +14 -26
  169. package/dist/llm/client.js +16 -2
  170. package/dist/llm/embedder.js +20 -29
  171. package/dist/llm/embedders/cache.js +3 -7
  172. package/dist/llm/embedders/local.js +42 -1
  173. package/dist/llm/embedders/remote.js +20 -8
  174. package/dist/llm/embedders/types.js +3 -7
  175. package/dist/llm/feature-gate.js +92 -56
  176. package/dist/llm/graph-extract.js +402 -31
  177. package/dist/llm/index-passes.js +44 -29
  178. package/dist/llm/memory-infer.js +30 -2
  179. package/dist/llm/metadata-enhance.js +3 -7
  180. package/dist/output/cli-hints.js +7 -4
  181. package/dist/output/context.js +60 -8
  182. package/dist/output/renderers.js +170 -194
  183. package/dist/output/shapes/curate.js +56 -0
  184. package/dist/output/shapes/distill.js +10 -0
  185. package/dist/output/shapes/env-list.js +19 -0
  186. package/dist/output/shapes/events.js +11 -0
  187. package/dist/output/shapes/helpers.js +424 -0
  188. package/dist/output/shapes/history.js +7 -0
  189. package/dist/output/shapes/passthrough.js +105 -0
  190. package/dist/output/shapes/proposal-accept.js +7 -0
  191. package/dist/output/shapes/proposal-diff.js +7 -0
  192. package/dist/output/shapes/proposal-list.js +7 -0
  193. package/dist/output/shapes/proposal-producer.js +11 -0
  194. package/dist/output/shapes/proposal-reject.js +7 -0
  195. package/dist/output/shapes/proposal-show.js +7 -0
  196. package/dist/output/shapes/registry-search.js +6 -0
  197. package/dist/output/shapes/registry.js +30 -0
  198. package/dist/output/shapes/search.js +6 -0
  199. package/dist/output/shapes/secret-list.js +19 -0
  200. package/dist/output/shapes/show.js +6 -0
  201. package/dist/output/shapes/vault-list.js +19 -0
  202. package/dist/output/shapes.js +51 -549
  203. package/dist/output/text/add.js +6 -0
  204. package/dist/output/text/clone.js +6 -0
  205. package/dist/output/text/config.js +6 -0
  206. package/dist/output/text/curate.js +6 -0
  207. package/dist/output/text/distill.js +7 -0
  208. package/dist/output/text/enable-disable.js +7 -0
  209. package/dist/output/text/events.js +10 -0
  210. package/dist/output/text/feedback.js +6 -0
  211. package/dist/output/text/helpers.js +1059 -0
  212. package/dist/output/text/history.js +7 -0
  213. package/dist/output/text/import.js +6 -0
  214. package/dist/output/text/index.js +6 -0
  215. package/dist/output/text/info.js +6 -0
  216. package/dist/output/text/init.js +6 -0
  217. package/dist/output/text/list.js +6 -0
  218. package/dist/output/text/proposal-producer.js +8 -0
  219. package/dist/output/text/proposal.js +12 -0
  220. package/dist/output/text/registry-commands.js +11 -0
  221. package/dist/output/text/registry.js +30 -0
  222. package/dist/output/text/remember.js +6 -0
  223. package/dist/output/text/remove.js +6 -0
  224. package/dist/output/text/save.js +6 -0
  225. package/dist/output/text/search.js +6 -0
  226. package/dist/output/text/show.js +6 -0
  227. package/dist/output/text/update.js +6 -0
  228. package/dist/output/text/upgrade.js +6 -0
  229. package/dist/output/text/vault.js +16 -0
  230. package/dist/output/text/wiki.js +15 -0
  231. package/dist/output/text/workflow.js +14 -0
  232. package/dist/output/text.js +44 -1329
  233. package/dist/registry/build-index.js +3 -0
  234. package/dist/registry/create-provider-registry.js +3 -0
  235. package/dist/registry/factory.js +4 -1
  236. package/dist/registry/origin-resolve.js +3 -0
  237. package/dist/registry/providers/index.js +3 -0
  238. package/dist/registry/providers/skills-sh.js +11 -2
  239. package/dist/registry/providers/static-index.js +10 -1
  240. package/dist/registry/providers/types.js +3 -24
  241. package/dist/registry/resolve.js +11 -16
  242. package/dist/registry/types.js +3 -0
  243. package/dist/scripts/migrate-storage.js +17767 -0
  244. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  245. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  246. package/dist/setup/detect.js +3 -0
  247. package/dist/setup/ripgrep-install.js +3 -0
  248. package/dist/setup/ripgrep-resolve.js +3 -0
  249. package/dist/setup/setup.js +306 -67
  250. package/dist/setup/steps.js +3 -15
  251. package/dist/sources/include.js +3 -0
  252. package/dist/sources/provider-factory.js +3 -11
  253. package/dist/sources/provider.js +3 -20
  254. package/dist/sources/providers/filesystem.js +19 -23
  255. package/dist/sources/providers/git.js +171 -21
  256. package/dist/sources/providers/index.js +3 -0
  257. package/dist/sources/providers/install-types.js +3 -13
  258. package/dist/sources/providers/npm.js +3 -4
  259. package/dist/sources/providers/provider-utils.js +3 -0
  260. package/dist/sources/providers/sync-from-ref.js +3 -11
  261. package/dist/sources/providers/tar-utils.js +3 -0
  262. package/dist/sources/providers/website.js +18 -22
  263. package/dist/sources/resolve.js +3 -0
  264. package/dist/sources/types.js +3 -0
  265. package/dist/sources/website-ingest.js +3 -0
  266. package/dist/tasks/backends/cron.js +3 -0
  267. package/dist/tasks/backends/exec-utils.js +3 -0
  268. package/dist/tasks/backends/index.js +3 -11
  269. package/dist/tasks/backends/launchd.js +4 -1
  270. package/dist/tasks/backends/schtasks.js +4 -1
  271. package/dist/tasks/parser.js +51 -38
  272. package/dist/tasks/resolveAkmBin.js +3 -0
  273. package/dist/tasks/runner.js +35 -9
  274. package/dist/tasks/schedule.js +20 -1
  275. package/dist/tasks/schema.js +5 -3
  276. package/dist/tasks/validator.js +6 -3
  277. package/dist/version.js +3 -0
  278. package/dist/wiki/wiki-templates.js +6 -3
  279. package/dist/wiki/wiki.js +4 -1
  280. package/dist/workflows/authoring.js +4 -1
  281. package/dist/workflows/cli.js +3 -0
  282. package/dist/workflows/db.js +140 -10
  283. package/dist/workflows/document-cache.js +3 -10
  284. package/dist/workflows/parser.js +3 -0
  285. package/dist/workflows/renderer.js +3 -0
  286. package/dist/workflows/runs.js +18 -1
  287. package/dist/workflows/schema.js +3 -0
  288. package/dist/workflows/scope-key.js +3 -0
  289. package/dist/workflows/validator.js +5 -9
  290. package/docs/README.md +7 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.5.md +2 -2
  293. package/docs/migration/release-notes/0.8.0.md +57 -5
  294. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  295. package/package.json +28 -11
  296. package/.github/LICENSE +0 -374
  297. package/dist/commands/help/help-accept.md +0 -9
  298. package/dist/commands/help/help-improve.md +0 -53
  299. package/dist/commands/help/help-reject.md +0 -8
  300. package/dist/commands/install-audit.js +0 -385
  301. package/dist/commands/vault.js +0 -310
  302. package/dist/indexer/match-contributors.js +0 -141
  303. package/dist/integrations/agent/pipeline.js +0 -39
  304. package/dist/integrations/agent/runners.js +0 -31
  305. package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
  306. /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
  307. /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
  308. /package/dist/{commands → assets}/help/help-propose.md +0 -0
  309. /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
  310. /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
  311. /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
  312. /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
  313. /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
@@ -1,385 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { filterNonEmptyStrings, toPosix } from "../core/common";
4
- const DEFAULT_INSTALL_AUDIT_CONFIG = {
5
- enabled: true,
6
- blockOnCritical: true,
7
- blockUnlistedRegistries: false,
8
- registryAllowlist: [],
9
- allowedFindings: [],
10
- };
11
- const MAX_SCANNED_FILE_BYTES = 256 * 1024;
12
- const LIFECYCLE_SCRIPT_NAMES = new Set([
13
- "preinstall",
14
- "install",
15
- "postinstall",
16
- "prepublish",
17
- "prepublishOnly",
18
- "prepare",
19
- ]);
20
- const TEXT_FILE_EXTENSIONS = new Set([
21
- ".cjs",
22
- ".cts",
23
- ".js",
24
- ".json",
25
- ".jsonc",
26
- ".jsx",
27
- ".mjs",
28
- ".md",
29
- ".ps1",
30
- ".py",
31
- ".rb",
32
- ".sh",
33
- ".toml",
34
- ".ts",
35
- ".tsx",
36
- ".txt",
37
- ".yaml",
38
- ".yml",
39
- ]);
40
- const BLOCKED_PACKAGE_DIRECTORIES = new Set(["node_modules", "venv", ".venv", "site-packages"]);
41
- const CONTENT_RULES = [
42
- {
43
- id: "prompt-ignore-previous-instructions",
44
- severity: "high",
45
- category: "prompt-injection",
46
- message: "Contains instructions to ignore prior prompts or instructions.",
47
- pattern: /\b(ignore|disregard|forget)\b[^.\n]{0,100}\b(previous|prior|earlier)\b[^.\n]{0,100}\b(instructions?|prompts?|messages?)\b/i,
48
- },
49
- {
50
- id: "prompt-reveal-hidden-secrets",
51
- severity: "critical",
52
- category: "prompt-injection",
53
- message: "Contains instructions to reveal hidden prompts or secrets.",
54
- pattern: /\b(?:reveal|print|dump|show|output|return|exfiltrat(?:e|ion))\b[^.\n]{0,60}\b(?:your|the)\b[^.\n]{0,40}\b(system prompt|hidden instructions?|developer message|api key|token|secret|password)\b/i,
55
- },
56
- {
57
- id: "prompt-bypass-guardrails",
58
- severity: "high",
59
- category: "prompt-injection",
60
- message: "Contains instructions to bypass safety or security controls.",
61
- pattern: /\b(bypass|disable|ignore)\b[^.\n]{0,100}\b(safety|security|guardrails|restrictions|policies)\b/i,
62
- },
63
- {
64
- id: "remote-shell-pipe",
65
- severity: "critical",
66
- category: "malicious-code",
67
- message: "Downloads remote content and pipes it directly into a shell.",
68
- pattern: /\b(curl|wget)\b[^\n|]{0,200}\|\s*(sh|bash|zsh)\b/i,
69
- },
70
- {
71
- id: "powershell-download-exec",
72
- severity: "critical",
73
- category: "malicious-code",
74
- message: "Downloads remote content and executes it in PowerShell.",
75
- pattern: /\b(Invoke-WebRequest|iwr|curl)\b[^\n|]{0,200}\|\s*(iex|Invoke-Expression)\b/i,
76
- },
77
- {
78
- id: "powershell-encoded-command",
79
- severity: "critical",
80
- category: "malicious-code",
81
- message: "Uses an encoded PowerShell command.",
82
- pattern: /\bpowershell(?:\.exe)?\b[^\n]{0,120}\s-(?:enc|encodedcommand)\b/i,
83
- },
84
- {
85
- id: "credential-exfiltration-language",
86
- severity: "high",
87
- category: "malicious-code",
88
- message: "Contains language associated with credential or secret exfiltration.",
89
- pattern: /\b(exfiltrat(?:e|ion)|harvest|steal)\b[^.\n]{0,120}\b(credentials?|tokens?|secrets?|ssh keys?|passwords?|cookies?)\b/i,
90
- },
91
- ];
92
- export function resolveInstallAuditConfig(config) {
93
- const installAudit = config?.security?.installAudit;
94
- const allowlist = filterNonEmptyStrings(installAudit?.registryAllowlist) ??
95
- filterNonEmptyStrings(installAudit?.registryWhitelist) ??
96
- [];
97
- return {
98
- enabled: installAudit?.enabled ?? DEFAULT_INSTALL_AUDIT_CONFIG.enabled,
99
- blockOnCritical: installAudit?.blockOnCritical ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockOnCritical,
100
- blockUnlistedRegistries: installAudit?.blockUnlistedRegistries ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockUnlistedRegistries,
101
- registryAllowlist: allowlist.map((entry) => entry.trim().toLowerCase()),
102
- allowedFindings: installAudit?.allowedFindings ?? DEFAULT_INSTALL_AUDIT_CONFIG.allowedFindings,
103
- };
104
- }
105
- export function enforceRegistryInstallPolicy(registryLabels, config, ref) {
106
- const resolved = resolveInstallAuditConfig(config);
107
- if (!resolved.blockUnlistedRegistries)
108
- return;
109
- if (resolved.registryAllowlist.length === 0) {
110
- throw new Error(`Install blocked for ${ref}: no registries are allowlisted. Configure security.installAudit.registryAllowlist or disable security.installAudit.blockUnlistedRegistries.`);
111
- }
112
- const matched = registryLabels.some((label) => resolved.registryAllowlist.includes(label.toLowerCase()));
113
- if (matched)
114
- return;
115
- throw new Error(`Install blocked for ${ref}: registry is not allowlisted. Allowed: ${resolved.registryAllowlist.join(", ")}. Seen: ${registryLabels.join(", ")}.`);
116
- }
117
- export function auditInstallCandidate(input) {
118
- const resolved = resolveInstallAuditConfig(input.config);
119
- if (!resolved.enabled) {
120
- return {
121
- enabled: false,
122
- passed: true,
123
- blocked: false,
124
- trusted: false,
125
- registryLabels: [...input.registryLabels],
126
- findings: [],
127
- scannedFiles: 0,
128
- scannedBytes: 0,
129
- summary: buildSummary([]),
130
- };
131
- }
132
- const findings = [];
133
- const counters = { scannedFiles: 0, scannedBytes: 0 };
134
- scanDirectory(input.rootDir, input.rootDir, findings, counters);
135
- const { findings: activeFindings, waivedFindings } = splitAllowedFindings(findings, input.ref, resolved.allowedFindings);
136
- const summary = buildSummary(activeFindings);
137
- const blocked = !input.trustThisInstall && resolved.blockOnCritical && summary.critical > 0;
138
- return {
139
- enabled: true,
140
- passed: activeFindings.length === 0,
141
- blocked,
142
- trusted: Boolean(input.trustThisInstall),
143
- registryLabels: [...input.registryLabels],
144
- findings: activeFindings,
145
- scannedFiles: counters.scannedFiles,
146
- scannedBytes: counters.scannedBytes,
147
- summary,
148
- ...(waivedFindings.length > 0 ? { waivedFindings } : {}),
149
- };
150
- }
151
- export function formatInstallAuditFailure(ref, report) {
152
- return formatInstallAuditFailureForAction(ref, report, "add");
153
- }
154
- export function formatInstallAuditFailureForAction(ref, report, action) {
155
- const lines = [`Security audit failed for ${ref}.`, formatInstallAuditSummary(report)];
156
- for (const finding of report.findings.slice(0, 5)) {
157
- lines.push(`- [${finding.severity}] ${finding.message}${finding.file ? ` (${finding.file})` : ""}`);
158
- }
159
- if (report.findings.length > 5) {
160
- lines.push(`- ${report.findings.length - 5} more finding(s) omitted`);
161
- }
162
- const trustCommand = action === "update" ? `akm update ${ref} --trust` : `akm add ${ref} --trust`;
163
- lines.push("Disable blocking with `security.installAudit.blockOnCritical = false`, or disable audits with `security.installAudit.enabled = false`." +
164
- ` Or pass --trust on a one-off '${trustCommand}' to bypass this audit for this ${action} only.`);
165
- return lines.join("\n");
166
- }
167
- export function formatInstallAuditSummary(report) {
168
- if (!report.enabled)
169
- return "Audit: disabled";
170
- const severitySummary = [];
171
- if (report.summary.critical > 0)
172
- severitySummary.push(`${report.summary.critical} critical`);
173
- if (report.summary.high > 0)
174
- severitySummary.push(`${report.summary.high} high`);
175
- if (report.summary.moderate > 0)
176
- severitySummary.push(`${report.summary.moderate} moderate`);
177
- if (report.summary.low > 0)
178
- severitySummary.push(`${report.summary.low} low`);
179
- const detail = severitySummary.length > 0 ? severitySummary.join(", ") : "no findings";
180
- const status = report.blocked ? "blocked" : report.passed ? "passed" : report.trusted ? "trusted" : "warnings";
181
- const waived = report.waivedFindings?.length ? `; waived ${report.waivedFindings.length}` : "";
182
- return `Audit: ${status} (${detail}; scanned ${report.scannedFiles} file${report.scannedFiles === 1 ? "" : "s"}${waived})`;
183
- }
184
- export function deriveRegistryLabels(input) {
185
- const labels = new Set();
186
- labels.add(input.source);
187
- if (input.source === "github")
188
- labels.add("github.com");
189
- if (input.source === "npm")
190
- labels.add("npm");
191
- addUrlLabels(labels, input.artifactUrl);
192
- addUrlLabels(labels, input.gitUrl);
193
- if (input.source === "github" && input.ref.startsWith("github:")) {
194
- labels.add("github");
195
- }
196
- return [...labels];
197
- }
198
- function scanDirectory(dir, rootDir, findings, counters) {
199
- let entries;
200
- try {
201
- entries = fs.readdirSync(dir, { withFileTypes: true });
202
- }
203
- catch {
204
- return;
205
- }
206
- for (const entry of entries) {
207
- if (entry.name === ".git")
208
- continue;
209
- const fullPath = path.join(dir, entry.name);
210
- if (entry.isDirectory()) {
211
- if (BLOCKED_PACKAGE_DIRECTORIES.has(entry.name)) {
212
- const relativePath = path.relative(rootDir, fullPath) || entry.name;
213
- findings.push({
214
- id: "bundled-package-directory",
215
- severity: "critical",
216
- category: "vendored-dependency",
217
- message: `Contains bundled dependency directory "${entry.name}".`,
218
- file: relativePath,
219
- snippet: relativePath,
220
- });
221
- continue;
222
- }
223
- scanDirectory(fullPath, rootDir, findings, counters);
224
- continue;
225
- }
226
- if (!entry.isFile())
227
- continue;
228
- scanFile(fullPath, rootDir, findings, counters);
229
- }
230
- }
231
- function scanFile(filePath, rootDir, findings, counters) {
232
- const ext = path.extname(filePath).toLowerCase();
233
- const basename = path.basename(filePath).toLowerCase();
234
- if (basename !== "package.json" && !TEXT_FILE_EXTENSIONS.has(ext))
235
- return;
236
- let fileSize;
237
- try {
238
- fileSize = fs.statSync(filePath).size;
239
- }
240
- catch {
241
- return;
242
- }
243
- const readSize = Math.min(fileSize, MAX_SCANNED_FILE_BYTES);
244
- const buf = Buffer.alloc(readSize);
245
- let bytesRead;
246
- try {
247
- const fd = fs.openSync(filePath, "r");
248
- try {
249
- bytesRead = fs.readSync(fd, buf, 0, readSize, 0);
250
- }
251
- finally {
252
- fs.closeSync(fd);
253
- }
254
- }
255
- catch {
256
- return;
257
- }
258
- if (bytesRead === 0)
259
- return;
260
- const bytes = buf.subarray(0, bytesRead);
261
- if (bytes.includes(0))
262
- return;
263
- counters.scannedFiles += 1;
264
- counters.scannedBytes += bytesRead;
265
- const content = bytes.toString("utf8");
266
- const relativePath = path.relative(rootDir, filePath) || path.basename(filePath);
267
- const genericContent = basename === "package.json" ? stripPackageJsonScripts(content) : content;
268
- for (const rule of CONTENT_RULES) {
269
- const match = genericContent.match(rule.pattern);
270
- if (!match)
271
- continue;
272
- findings.push({
273
- id: rule.id,
274
- severity: rule.severity,
275
- category: rule.category,
276
- message: rule.message,
277
- file: relativePath,
278
- snippet: clipSnippet(match[0]),
279
- });
280
- }
281
- if (basename === "package.json") {
282
- scanPackageJson(content, relativePath, findings);
283
- }
284
- }
285
- function stripPackageJsonScripts(content) {
286
- let parsed;
287
- try {
288
- parsed = JSON.parse(content);
289
- }
290
- catch {
291
- return content;
292
- }
293
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
294
- return content;
295
- const packageJson = { ...parsed };
296
- delete packageJson.scripts;
297
- return JSON.stringify(packageJson, null, 2);
298
- }
299
- function scanPackageJson(content, relativePath, findings) {
300
- let parsed;
301
- try {
302
- parsed = JSON.parse(content);
303
- }
304
- catch {
305
- return;
306
- }
307
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
308
- return;
309
- const scripts = parsed.scripts;
310
- if (typeof scripts !== "object" || scripts === null || Array.isArray(scripts))
311
- return;
312
- for (const [name, command] of Object.entries(scripts)) {
313
- if (!LIFECYCLE_SCRIPT_NAMES.has(name) || typeof command !== "string")
314
- continue;
315
- for (const rule of CONTENT_RULES) {
316
- if (!rule.pattern.test(command))
317
- continue;
318
- findings.push({
319
- id: `lifecycle-${name}-${rule.id}`,
320
- severity: rule.severity,
321
- category: "install-script",
322
- message: `Lifecycle script "${name}" is suspicious: ${rule.message.toLowerCase()}`,
323
- file: relativePath,
324
- snippet: clipSnippet(command),
325
- });
326
- }
327
- }
328
- }
329
- function clipSnippet(value) {
330
- const normalized = value.replace(/\s+/g, " ").trim();
331
- return normalized.length <= 140 ? normalized : `${normalized.slice(0, 137)}...`;
332
- }
333
- function buildSummary(findings) {
334
- const summary = { low: 0, moderate: 0, high: 0, critical: 0, total: findings.length };
335
- for (const finding of findings) {
336
- summary[finding.severity] += 1;
337
- }
338
- return summary;
339
- }
340
- function splitAllowedFindings(findings, ref, allowedFindings) {
341
- const active = [];
342
- const waived = [];
343
- for (const finding of findings) {
344
- if (matchesAllowedFinding(finding, ref, allowedFindings)) {
345
- waived.push(finding);
346
- continue;
347
- }
348
- active.push(finding);
349
- }
350
- return { findings: active, waivedFindings: waived };
351
- }
352
- function matchesAllowedFinding(finding, ref, allowedFindings) {
353
- // Normalize paths so a waiver written against `scripts/setup.sh` matches
354
- // a finding emitted as `./scripts/setup.sh` or `scripts//setup.sh`. On
355
- // Windows we also fold case, mirroring `isWithin`'s comparison rules.
356
- const findingPathNormalized = normalizeWaiverPath(finding.file);
357
- return allowedFindings.some((allowed) => {
358
- if (allowed.id !== finding.id)
359
- return false;
360
- if (allowed.ref && allowed.ref !== ref)
361
- return false;
362
- if (allowed.path && normalizeWaiverPath(allowed.path) !== findingPathNormalized)
363
- return false;
364
- return true;
365
- });
366
- }
367
- function normalizeWaiverPath(value) {
368
- if (!value)
369
- return value;
370
- // Strip a leading `./` and POSIX-ify after path.normalize so Windows path
371
- // separators don't trigger spurious mismatches.
372
- const normalized = toPosix(path.normalize(value)).replace(/^\.\/+/, "");
373
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
374
- }
375
- function addUrlLabels(labels, rawUrl) {
376
- if (!rawUrl)
377
- return;
378
- try {
379
- const parsed = new URL(rawUrl);
380
- labels.add(parsed.hostname.toLowerCase());
381
- }
382
- catch {
383
- // Ignore non-URL refs (for example git@host:path)
384
- }
385
- }
@@ -1,310 +0,0 @@
1
- /**
2
- * Vault asset type — secret storage backed by `.env` files.
3
- *
4
- * Invariant: vault values must never be written to stdout, returned through
5
- * the indexer, the `akm show` renderer, or any structured output channel.
6
- * The supported load paths are:
7
- *
8
- * - `source "$(akm vault path vault:<name>)"` — direct shell loading path.
9
- * - `injectIntoEnv(vaultPath, target)` / `loadEnv(vaultPath)` — programmatic
10
- * APIs for modules that need values in process memory.
11
- *
12
- * Value parsing is delegated to the `dotenv` package — we deliberately do not
13
- * implement our own quoting/escaping rules for security-sensitive content.
14
- */
15
- import fs from "node:fs";
16
- import path from "node:path";
17
- import dotenv from "dotenv";
18
- import { writeFileAtomic } from "../core/common";
19
- /** Matches a KEY=value assignment line, capturing only the key. */
20
- const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
21
- /** Scan lines and return KEY names in file order, without duplicates. */
22
- function scanKeys(text) {
23
- const keys = [];
24
- const seen = new Set();
25
- for (const line of text.split(/\r?\n/)) {
26
- const m = line.match(ASSIGN_RE);
27
- if (!m)
28
- continue;
29
- const key = m[1];
30
- if (seen.has(key))
31
- continue;
32
- seen.add(key);
33
- keys.push(key);
34
- }
35
- return keys;
36
- }
37
- /**
38
- * Scan lines and return start-of-line `#` comments (with the leading `#` and
39
- * any leading whitespace stripped). Inline/trailing `#` after an assignment is
40
- * never extracted.
41
- */
42
- function scanComments(text) {
43
- const comments = [];
44
- for (const line of text.split(/\r?\n/)) {
45
- const trimmed = line.trimStart();
46
- if (trimmed.startsWith("#")) {
47
- comments.push(trimmed.slice(1).trimStart());
48
- }
49
- }
50
- return comments;
51
- }
52
- /**
53
- * Read and return ONLY non-secret metadata (keys + start-of-line comments).
54
- *
55
- * The function reads the whole file into memory (same as any dotenv parser)
56
- * but deliberately does not parse values — the LHS-only regex scanners above
57
- * ensure no value content is retained or returned. The guarantee is that
58
- * values never leave this function.
59
- */
60
- export function listKeys(vaultPath) {
61
- if (!fs.existsSync(vaultPath))
62
- return { keys: [], comments: [] };
63
- const text = fs.readFileSync(vaultPath, "utf8");
64
- return { keys: scanKeys(text), comments: scanComments(text) };
65
- }
66
- /**
67
- * Return structured `entries` pairing each key with the nearest preceding
68
- * comment line (if any). This replaces the parallel `keys[]` + `comments[]`
69
- * shape used internally by `listKeys` with a single merged array, which is
70
- * easier for callers to consume (QA #35).
71
- *
72
- * Values are never included — the same privacy guarantee as `listKeys`.
73
- */
74
- export function listEntries(vaultPath) {
75
- if (!fs.existsSync(vaultPath))
76
- return [];
77
- const text = fs.readFileSync(vaultPath, "utf8");
78
- const lines = text.split(/\r?\n/);
79
- const seen = new Set();
80
- const entries = [];
81
- let pendingComment;
82
- for (const line of lines) {
83
- const trimmed = line.trimStart();
84
- if (trimmed.startsWith("#")) {
85
- // Capture the most recent comment before a key
86
- pendingComment = trimmed.slice(1).trimStart() || undefined;
87
- continue;
88
- }
89
- const m = line.match(ASSIGN_RE);
90
- if (m) {
91
- const key = m[1];
92
- if (!seen.has(key)) {
93
- seen.add(key);
94
- const entry = { key };
95
- if (pendingComment)
96
- entry.comment = pendingComment;
97
- entries.push(entry);
98
- }
99
- pendingComment = undefined;
100
- }
101
- else {
102
- // Any non-comment, non-assignment line (including blank lines)
103
- // breaks "nearest preceding comment line" association.
104
- pendingComment = undefined;
105
- }
106
- }
107
- return entries;
108
- }
109
- /**
110
- * Read all KEY=value pairs from a vault file. Intended for programmatic
111
- * callers that need to inject values into a process environment. Callers
112
- * MUST NOT write the returned values to stdout or any logged output.
113
- *
114
- * Value parsing (quoting, escapes, multi-line, etc.) is delegated to dotenv.
115
- */
116
- export function loadEnv(vaultPath) {
117
- if (!fs.existsSync(vaultPath))
118
- return {};
119
- const buf = fs.readFileSync(vaultPath);
120
- return dotenv.parse(buf);
121
- }
122
- /**
123
- * Load a vault and assign its values into `target` (defaults to `process.env`).
124
- * Returns the list of keys that were set so the caller can log/observe without
125
- * touching values.
126
- *
127
- * Existing keys in `target` are overwritten — callers who want to preserve
128
- * pre-existing environment variables should filter before calling.
129
- */
130
- export function injectIntoEnv(vaultPath, target = process.env) {
131
- const env = loadEnv(vaultPath);
132
- for (const [key, value] of Object.entries(env)) {
133
- target[key] = value;
134
- }
135
- return Object.keys(env);
136
- }
137
- /**
138
- * Serialise a vault's values as a POSIX shell script of `export KEY='value'`
139
- * lines, with single-quote escaping (`'\''`). Every line is an assignment of
140
- * a literal string — there is no expansion, command substitution, or
141
- * non-assignment content, so sourcing the output is safe regardless of what
142
- * the vault file contains.
143
- *
144
- * Retained for programmatic callers/tests that need a literal export script.
145
- */
146
- export function buildShellExportScript(vaultPath) {
147
- const env = loadEnv(vaultPath);
148
- const lines = [];
149
- for (const [key, value] of Object.entries(env)) {
150
- // Defence in depth: dotenv already validates key shape, but reject any
151
- // key we wouldn't be able to export safely.
152
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
153
- continue;
154
- const escaped = value.replace(/'/g, "'\\''");
155
- lines.push(`export ${key}='${escaped}'`);
156
- }
157
- return lines.length > 0 ? `${lines.join("\n")}\n` : "";
158
- }
159
- /**
160
- * Set a key in the vault file, preserving line order and comments. Creates
161
- * the file (and parent directory) if it does not exist.
162
- *
163
- * `quoteValue` picks the safest representation that dotenv round-trips:
164
- * single-quoted when the value has no `'`, double-quoted when it has `'` but
165
- * no `"` and no literal `\n`/`\r` escape sequences, and unquoted only for
166
- * values that contain no characters requiring escaping (see quoteValue for
167
- * the full rule set). Values containing newlines or both quote types are
168
- * rejected outright. Round-trip safety is enforced by the test suite.
169
- *
170
- * When `comment` is provided it is written as a `# <comment>` line
171
- * immediately before the `KEY=value` line:
172
- * - New key: the comment line is inserted just before the appended key.
173
- * - Existing key: if the preceding line is already a comment it is replaced
174
- * with the new comment; otherwise a new comment line is inserted.
175
- * When `comment` is absent the surrounding comment lines are left unchanged.
176
- */
177
- export function setKey(vaultPath, key, value, comment) {
178
- validateKeyName(key);
179
- if (comment !== undefined && /[\r\n]/.test(comment)) {
180
- throw new Error("Vault key comment cannot contain newline characters.");
181
- }
182
- ensureParentDir(vaultPath);
183
- const existing = fs.existsSync(vaultPath) ? fs.readFileSync(vaultPath, "utf8") : "";
184
- const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
185
- const formatted = `${key}=${quoteValue(value)}`;
186
- let replaced = false;
187
- for (let i = 0; i < lines.length; i++) {
188
- const m = lines[i].match(ASSIGN_RE);
189
- if (m && m[1] === key) {
190
- lines[i] = formatted;
191
- replaced = true;
192
- if (comment !== undefined) {
193
- const commentLine = `# ${comment}`;
194
- const prevIsComment = i > 0 && lines[i - 1].trimStart().startsWith("#");
195
- if (prevIsComment) {
196
- lines[i - 1] = commentLine;
197
- }
198
- else {
199
- lines.splice(i, 0, commentLine);
200
- }
201
- }
202
- break;
203
- }
204
- }
205
- if (!replaced) {
206
- if (comment !== undefined) {
207
- const commentLine = `# ${comment}`;
208
- if (lines.length > 0 && lines[lines.length - 1] === "") {
209
- lines[lines.length - 1] = commentLine;
210
- lines.push(formatted);
211
- lines.push("");
212
- }
213
- else {
214
- lines.push(commentLine);
215
- lines.push(formatted);
216
- }
217
- }
218
- else if (lines.length > 0 && lines[lines.length - 1] === "") {
219
- lines[lines.length - 1] = formatted;
220
- lines.push("");
221
- }
222
- else {
223
- lines.push(formatted);
224
- }
225
- }
226
- let out = lines.join("\n");
227
- if (!out.endsWith("\n"))
228
- out += "\n";
229
- writeFileAtomic(vaultPath, out, 0o600);
230
- }
231
- /** Remove a key from the vault file. Returns true if the key was present. */
232
- export function unsetKey(vaultPath, key) {
233
- if (!fs.existsSync(vaultPath))
234
- return false;
235
- const text = fs.readFileSync(vaultPath, "utf8");
236
- const lines = text.split(/\r?\n/);
237
- const kept = [];
238
- let removed = false;
239
- for (const line of lines) {
240
- const m = line.match(ASSIGN_RE);
241
- if (m && m[1] === key) {
242
- removed = true;
243
- continue;
244
- }
245
- kept.push(line);
246
- }
247
- if (!removed)
248
- return false;
249
- let out = kept.join("\n");
250
- if (out.length > 0 && !out.endsWith("\n"))
251
- out += "\n";
252
- writeFileAtomic(vaultPath, out, 0o600);
253
- return true;
254
- }
255
- /** Create an empty vault file (does nothing if it already exists). */
256
- export function createVault(vaultPath) {
257
- ensureParentDir(vaultPath);
258
- if (fs.existsSync(vaultPath))
259
- return;
260
- writeFileAtomic(vaultPath, "", 0o600);
261
- }
262
- /**
263
- * Characters that are safe in an UNquoted dotenv value AND are not
264
- * metacharacters in POSIX shells. Anything outside this set forces quoting,
265
- * which is defense-in-depth for any caller that might ever `source` the
266
- * vault file directly instead of going through `akm vault path`.
267
- */
268
- const UNQUOTED_SAFE_RE = /^[A-Za-z0-9_.:/@%+,-]+$/;
269
- /**
270
- * Quote a value for safe storage in a .env file that round-trips through
271
- * `dotenv.parse` AND is safe if the file is ever `source`d by a POSIX shell.
272
- *
273
- * Strategy:
274
- * - empty → empty
275
- * - all-safe chars (alnum + `_.:/@%+,-`) → unquoted
276
- * - no `'` → single-quote (dotenv and shell both treat single-quoted
277
- * content literally: no expansion, no escapes)
278
- * - no `"` and no literal `\n`/`\r` escape sequence → double-quote
279
- * (dotenv unescapes `\n`/`\r` on read, so we
280
- * can't double-quote a value that contains
281
- * those literal sequences)
282
- * - newlines or both quote types → reject
283
- *
284
- * dotenv intentionally does NOT support `\"` inside double-quoted values, so
285
- * we never produce that pattern.
286
- */
287
- function quoteValue(value) {
288
- if (value.length === 0)
289
- return "";
290
- if (/[\n\r]/.test(value)) {
291
- throw new Error("Vault values cannot contain literal newlines.");
292
- }
293
- if (UNQUOTED_SAFE_RE.test(value))
294
- return value;
295
- if (!value.includes("'"))
296
- return `'${value}'`;
297
- if (!value.includes('"') && !/\\[nr]/.test(value))
298
- return `"${value}"`;
299
- throw new Error("Vault value contains both single and double quote characters; not supported.");
300
- }
301
- function validateKeyName(key) {
302
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
303
- throw new Error(`Invalid vault key name: "${key}". Must match [A-Za-z_][A-Za-z0-9_]*`);
304
- }
305
- }
306
- function ensureParentDir(filePath) {
307
- const dir = path.dirname(filePath);
308
- if (!fs.existsSync(dir))
309
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
310
- }