aienvmp 0.1.7 → 0.1.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.8
4
+
5
+ - Added optional `sync --security` / `scan --security` vulnerability summary collection.
6
+ - Added read-only npm audit parsing for light SBOM security awareness.
7
+ - Exposed security summaries to AI-facing context, handoff, `AIENV.md`, and the dashboard.
8
+
3
9
  ## 0.1.7
4
10
 
5
11
  - Added stale open intent warnings for long-running environment change plans.
package/README.md CHANGED
@@ -21,10 +21,11 @@ npx aienvmp handoff
21
21
  npx aienvmp handoff --record --actor agent:codex
22
22
  ```
23
23
 
24
- Optional deeper read-only inventory:
24
+ Optional read-only checks:
25
25
 
26
26
  ```bash
27
27
  npx aienvmp sync --deep
28
+ npx aienvmp sync --security
28
29
  ```
29
30
 
30
31
  ## Output
@@ -68,6 +69,7 @@ aienvmp doctor --ci # strict CI check
68
69
  - lightweight
69
70
  - one advisory engine, optional enforcement with `doctor --ci`
70
71
  - non-blocking unless strict mode is explicitly requested
72
+ - security checks are opt-in and read-only
71
73
 
72
74
  ## Development
73
75
 
package/ROADMAP.md CHANGED
@@ -7,6 +7,7 @@
7
7
  - Strengthen trust states: observed, planned, changed, review, verified, stale
8
8
  - Detect multi-agent environment intent conflicts
9
9
  - Keep one advisory decision engine with optional strict enforcement
10
+ - Keep vulnerability checks opt-in and read-only
10
11
  - Stabilize `.aienvmp/manifest.json` and JSON command schemas
11
12
  - Keep `sync`, `context`, and `handoff` as the simple core flow
12
13
  - Improve the dashboard for 10-second human review
@@ -20,6 +21,9 @@
20
21
  - Global tool inventory:
21
22
  - richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
22
23
  - optional `--deep` scanners for more toolchains
24
+ - Security summaries:
25
+ - Python vulnerability summary via optional scanner detection
26
+ - OS/container vulnerability summaries through optional external tools
23
27
  - Conflict detection:
24
28
  - package manager policy vs lockfile mismatch
25
29
  - monorepo/project boundary aware intent targets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -74,7 +74,7 @@ function printUsage() {
74
74
  console.log(`aienvmp - AI-first env map + lightweight runtime SBOM
75
75
 
76
76
  Usage:
77
- aienvmp sync [--dir .] [--json] [--quiet] [--deep]
77
+ aienvmp sync [--dir .] [--json] [--quiet] [--deep] [--security]
78
78
  aienvmp context [--dir .] [--json]
79
79
  aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
80
80
 
@@ -87,7 +87,7 @@ Common:
87
87
 
88
88
  Advanced:
89
89
  aienvmp init [--dir .]
90
- aienvmp scan [--dir .] [--deep]
90
+ aienvmp scan [--dir .] [--deep] [--security]
91
91
  aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
92
92
  aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
93
93
  aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
@@ -25,6 +25,7 @@ export async function contextWorkspace(args) {
25
25
  packageManagers: manifest.packageManagers,
26
26
  containers: manifest.containers,
27
27
  inventory: inventorySummary(manifest.inventory),
28
+ security: securitySummary(manifest.security),
28
29
  projectHints: manifest.projectHints,
29
30
  warnings,
30
31
  policy,
@@ -37,6 +38,14 @@ export async function contextWorkspace(args) {
37
38
  console.log(renderContext(manifest, timeline, warnings, intents, policy));
38
39
  }
39
40
 
41
+ function securitySummary(security = {}) {
42
+ return {
43
+ mode: security.mode || "basic",
44
+ enabled: security.enabled === true,
45
+ summary: security.summary || { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 }
46
+ };
47
+ }
48
+
40
49
  function inventorySummary(inventory = {}) {
41
50
  const tools = inventory.tools || {};
42
51
  return {
@@ -53,6 +53,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
53
53
  docker: manifest.containers?.docker ? "available" : "not detected"
54
54
  },
55
55
  inventory: inventorySummary(manifest.inventory),
56
+ security: securitySummary(manifest.security),
56
57
  policy: {
57
58
  node: policy.node || "not set",
58
59
  python: policy.python || "not set",
@@ -74,6 +75,14 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
74
75
  };
75
76
  }
76
77
 
78
+ function securitySummary(security = {}) {
79
+ return {
80
+ mode: security.mode || "basic",
81
+ enabled: security.enabled === true,
82
+ summary: security.summary || { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 }
83
+ };
84
+ }
85
+
77
86
  function inventorySummary(inventory = {}) {
78
87
  const tools = inventory.tools || {};
79
88
  return {
@@ -11,7 +11,7 @@ export async function scanWorkspace(args) {
11
11
  if (previous && await exists(currentPath)) {
12
12
  await fs.copyFile(currentPath, previousManifestPath(dir));
13
13
  }
14
- const manifest = await buildManifest(dir, { deep: args.deep });
14
+ const manifest = await buildManifest(dir, { deep: args.deep, security: args.security });
15
15
  await writeJson(currentPath, manifest);
16
16
  const changes = diffManifests(previous, manifest);
17
17
  for (const change of changes) {
package/src/doctor.js CHANGED
@@ -42,12 +42,25 @@ export function diagnose(manifest, context = {}) {
42
42
  message: "Environment snapshot is older than 24 hours. Run `aienvmp sync` before changing the environment."
43
43
  });
44
44
  }
45
+ warnings.push(...securityWarnings(manifest.security));
45
46
  warnings.push(...coordinationWarnings(context.intents || []));
46
47
  warnings.push(...staleIntentWarnings(context.intents || []));
47
48
  warnings.push(...handoffWarnings(context.timeline || []));
48
49
  return warnings;
49
50
  }
50
51
 
52
+ export function securityWarnings(security = {}) {
53
+ if (!security.enabled || !security.summary) return [];
54
+ const summary = security.summary;
55
+ if (Number(summary.critical || 0) > 0 || Number(summary.high || 0) > 0) {
56
+ return [{
57
+ code: "security-vulnerabilities",
58
+ message: `Security scan found ${summary.critical || 0} critical and ${summary.high || 0} high vulnerabilities. Review before changing or deploying the environment.`
59
+ }];
60
+ }
61
+ return [];
62
+ }
63
+
51
64
  export function coordinationWarnings(intents = []) {
52
65
  const warnings = [];
53
66
  const byTarget = new Map();
package/src/manifest.js CHANGED
@@ -5,6 +5,7 @@ import { commandOutput, commandVersion } from "./shell.js";
5
5
  import { exists } from "./fsutil.js";
6
6
  import { observedTrust } from "./trust.js";
7
7
  import { scanGlobalInventory } from "./inventory.js";
8
+ import { scanSecurity } from "./security.js";
8
9
 
9
10
  export async function buildManifest(dir, options = {}) {
10
11
  const now = new Date().toISOString();
@@ -14,7 +15,7 @@ export async function buildManifest(dir, options = {}) {
14
15
  generatedAt: now,
15
16
  generatedBy: {
16
17
  name: "aienvmp",
17
- command: options.deep ? "aienvmp sync --deep" : "aienvmp sync"
18
+ command: generatedCommand(options)
18
19
  },
19
20
  trust: observedTrust(new Date(now)),
20
21
  workspace: {
@@ -27,6 +28,7 @@ export async function buildManifest(dir, options = {}) {
27
28
  containers: await scanContainers(),
28
29
  projectHints: await scanProjectHints(dir),
29
30
  inventory: await scanGlobalInventory({ deep: options.deep }),
31
+ security: await scanSecurity(dir, { security: options.security }),
30
32
  agentFiles: await scanAgentFiles(dir),
31
33
  agentProtocol: {
32
34
  sourceOfTruth: "AIENV.md",
@@ -78,6 +80,10 @@ export async function buildManifest(dir, options = {}) {
78
80
  uvTools: "uv tool list",
79
81
  brew: "brew list --versions"
80
82
  },
83
+ security: {
84
+ mode: "basic by default; vulnerability summaries only when requested",
85
+ npmAudit: "npm audit --json"
86
+ },
81
87
  projectHints: [
82
88
  ".nvmrc",
83
89
  ".python-version",
@@ -96,6 +102,10 @@ export async function buildManifest(dir, options = {}) {
96
102
  return manifest;
97
103
  }
98
104
 
105
+ function generatedCommand(options = {}) {
106
+ return ["aienvmp", "sync", options.deep ? "--deep" : "", options.security ? "--security" : ""].filter(Boolean).join(" ");
107
+ }
108
+
99
109
  async function scanOS() {
100
110
  return {
101
111
  platform: os.platform(),
package/src/render.js CHANGED
@@ -31,6 +31,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
31
31
  pushMap(lines, "Containers", manifest.containers);
32
32
  lines.push("## Global Tool Inventory", "");
33
33
  lines.push(...inventoryLines(manifest.inventory), "");
34
+ lines.push("## Security Summary", "");
35
+ lines.push(...securityLines(manifest.security), "");
34
36
  lines.push("## Project Requirements And Hints", "");
35
37
  pushMap(lines, "Detected", manifest.projectHints);
36
38
  lines.push("## Drift And Warnings", "");
@@ -96,6 +98,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
96
98
  `Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
97
99
  `Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
98
100
  `Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
101
+ `Security: ${manifest.security?.mode || "basic"}${manifest.security?.enabled ? ` enabled (${manifest.security.summary?.total || 0} vulnerabilities)` : " disabled"}`,
99
102
  `Policy Node: ${policy.node || "not set"}`,
100
103
  `Policy Python: ${policy.python || "not set"}`,
101
104
  `Policy Package Manager: ${policy.packageManager || "not set"}`,
@@ -135,6 +138,7 @@ export function renderHandoff(handoff) {
135
138
  `- Python: ${handoff.safeRuntime.python}`,
136
139
  `- Docker: ${handoff.safeRuntime.docker}`,
137
140
  `- Inventory: ${handoff.inventory?.mode || "basic"}${handoff.inventory?.enabled ? " enabled" : " disabled"}`,
141
+ `- Security: ${handoff.security?.mode || "basic"}${handoff.security?.enabled ? ` enabled (${handoff.security.summary?.total || 0} vulnerabilities)` : " disabled"}`,
138
142
  "",
139
143
  "Open intents:",
140
144
  ...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
@@ -213,6 +217,9 @@ const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(
213
217
  const inventoryGroups=manifest.inventory?.tools||{};
214
218
  const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
215
219
  const inventoryHtml=manifest.inventory?.enabled?('<table>'+Object.entries(inventoryGroups).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${Array.isArray(v)?v.length:0} tools</code></td></tr>\`).join('')+'</table>'):'<div class="okline">Deep global inventory is off. Run <code>aienvmp sync --deep</code> when an AI needs global tool awareness.</div>';
220
+ const sec=manifest.security||{};
221
+ const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
222
+ const securityHtml=sec.enabled?\`<table><tr><th>Total</th><td><code>\${esc(secSummary.total||0)}</code></td></tr><tr><th>Critical</th><td><code>\${esc(secSummary.critical||0)}</code></td></tr><tr><th>High</th><td><code>\${esc(secSummary.high||0)}</code></td></tr><tr><th>Moderate</th><td><code>\${esc(secSummary.moderate||0)}</code></td></tr><tr><th>Low</th><td><code>\${esc(secSummary.low||0)}</code></td></tr></table>\`:'<div class="okline">Security scan is off. Run <code>aienvmp sync --security</code> for read-only vulnerability summary.</div>';
216
223
  const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
217
224
  const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
218
225
  const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
@@ -257,6 +264,7 @@ document.getElementById('app').innerHTML=\`
257
264
  \${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
258
265
  \${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
259
266
  \${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
267
+ \${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
260
268
  </div>
261
269
  <aside>
262
270
  \${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
@@ -326,6 +334,19 @@ function inventoryLines(inventory = {}) {
326
334
  return lines;
327
335
  }
328
336
 
337
+ function securityLines(security = {}) {
338
+ if (!security.enabled) return ["- Mode: basic", "- Security scan is disabled. Run `aienvmp sync --security` when vulnerability context is needed."];
339
+ const summary = security.summary || {};
340
+ return [
341
+ "- Mode: security",
342
+ `- Total vulnerabilities: ${summary.total || 0}`,
343
+ `- Critical: ${summary.critical || 0}`,
344
+ `- High: ${summary.high || 0}`,
345
+ `- Moderate: ${summary.moderate || 0}`,
346
+ `- Low: ${summary.low || 0}`
347
+ ];
348
+ }
349
+
329
350
  function policyLines(policy) {
330
351
  const lines = [];
331
352
  if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
@@ -0,0 +1,90 @@
1
+ import { exists } from "./fsutil.js";
2
+ import { commandResult } from "./shell.js";
3
+ import path from "node:path";
4
+
5
+ const SEVERITIES = ["critical", "high", "moderate", "low", "info"];
6
+
7
+ export async function scanSecurity(dir, options = {}) {
8
+ if (!options.security) {
9
+ return {
10
+ mode: "basic",
11
+ enabled: false,
12
+ note: "Run `aienvmp sync --security` to collect read-only vulnerability summaries."
13
+ };
14
+ }
15
+
16
+ const npmAudit = await scanNpmAudit(dir);
17
+ return {
18
+ mode: "security",
19
+ enabled: true,
20
+ note: "Read-only vulnerability summary. Use for review and CI policy, not automatic fixes.",
21
+ scanners: {
22
+ npmAudit
23
+ },
24
+ summary: summarizeScanners({ npmAudit })
25
+ };
26
+ }
27
+
28
+ export async function scanNpmAudit(dir) {
29
+ const hasPackageJson = await exists(path.join(dir, "package.json"));
30
+ if (!hasPackageJson) return unavailable("npm-audit", "package.json not found");
31
+
32
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
33
+ const result = await commandResult(command, ["audit", "--json"], {
34
+ cwd: dir,
35
+ timeout: 15000,
36
+ maxBuffer: 4 * 1024 * 1024
37
+ });
38
+ const parsed = parseNpmAudit(result.stdout);
39
+ if (!parsed.available) {
40
+ return unavailable("npm-audit", result.stderr || "npm audit did not return parseable JSON");
41
+ }
42
+ return {
43
+ scanner: "npm-audit",
44
+ available: true,
45
+ ok: result.ok,
46
+ exitCode: result.code,
47
+ summary: parsed.summary,
48
+ vulnerablePackages: parsed.vulnerablePackages
49
+ };
50
+ }
51
+
52
+ export function parseNpmAudit(raw) {
53
+ try {
54
+ const parsed = JSON.parse(raw || "{}");
55
+ const metadata = parsed.metadata || {};
56
+ const vulnerabilities = parsed.vulnerabilities || {};
57
+ const severityCounts = Object.fromEntries(SEVERITIES.map((severity) => [severity, Number(metadata.vulnerabilities?.[severity] || 0)]));
58
+ const total = Number(metadata.vulnerabilities?.total || Object.values(severityCounts).reduce((sum, value) => sum + value, 0));
59
+ const vulnerablePackages = Object.entries(vulnerabilities).slice(0, 20).map(([name, value]) => ({
60
+ name,
61
+ severity: value.severity || "unknown",
62
+ viaCount: Array.isArray(value.via) ? value.via.length : 0,
63
+ fixAvailable: Boolean(value.fixAvailable)
64
+ }));
65
+ return {
66
+ available: true,
67
+ summary: { total, ...severityCounts },
68
+ vulnerablePackages
69
+ };
70
+ } catch {
71
+ return { available: false };
72
+ }
73
+ }
74
+
75
+ export function summarizeScanners(scanners = {}) {
76
+ const summary = { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
77
+ for (const scanner of Object.values(scanners)) {
78
+ if (!scanner?.available) continue;
79
+ for (const key of Object.keys(summary)) summary[key] += Number(scanner.summary?.[key] || 0);
80
+ }
81
+ return summary;
82
+ }
83
+
84
+ function unavailable(scanner, reason) {
85
+ return {
86
+ scanner,
87
+ available: false,
88
+ reason
89
+ };
90
+ }
package/src/shell.js CHANGED
@@ -20,6 +20,7 @@ export async function commandOutput(command, args = [], options = {}) {
20
20
  const { stdout } = await execFileAsync(command, args, {
21
21
  timeout: options.timeout || 2500,
22
22
  maxBuffer: options.maxBuffer || 1024 * 1024,
23
+ cwd: options.cwd,
23
24
  windowsHide: true
24
25
  });
25
26
  return stdout.trim();
@@ -28,6 +29,25 @@ export async function commandOutput(command, args = [], options = {}) {
28
29
  }
29
30
  }
30
31
 
32
+ export async function commandResult(command, args = [], options = {}) {
33
+ try {
34
+ const { stdout, stderr } = await execFileAsync(command, args, {
35
+ timeout: options.timeout || 5000,
36
+ maxBuffer: options.maxBuffer || 2 * 1024 * 1024,
37
+ cwd: options.cwd,
38
+ windowsHide: true
39
+ });
40
+ return { ok: true, code: 0, stdout: stdout.trim(), stderr: stderr.trim() };
41
+ } catch (error) {
42
+ return {
43
+ ok: false,
44
+ code: typeof error.code === "number" ? error.code : 1,
45
+ stdout: String(error.stdout || "").trim(),
46
+ stderr: String(error.stderr || error.message || "").trim()
47
+ };
48
+ }
49
+ }
50
+
31
51
  export function firstVersion(text) {
32
52
  const match = String(text).match(/(?:v)?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
33
53
  return match ? match[1] : null;