aienvmp 0.1.45 → 0.1.46

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/BUGFIXES.md CHANGED
@@ -58,6 +58,12 @@ Short record of bugs, fixes, and follow-up checks.
58
58
  - Fix: `aienvmp checkpoint` now performs the post-change loop and records an explicit sync ledger entry so follow-ups can be closed.
59
59
  - Verification: checkpoint tests cover text/JSON output, status artifact refresh, timeline entries, handoff recording, and cleared follow-ups.
60
60
 
61
+ ### Light SBOM risk required too much parsing
62
+
63
+ - Issue: AI consumers could see package and vulnerability details, but had to infer risk level and next commands from several nested fields.
64
+ - Fix: `lightSbom.riskSummary` and preflight `sbomRisk` now provide a compact risk score, signals, review targets, and advisory next steps.
65
+ - Verification: tests cover risk scoring, top-risk severity fallback, scanner-off guidance, status/context exposure, dashboard rendering, and recommended actions.
66
+
61
67
  ## Template
62
68
 
63
69
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.46
4
+
5
+ - Added `lightSbom.riskSummary` with compact risk level, score, scanner state, signals, review targets, and next commands.
6
+ - Added `sbomRisk` to the shared AI preflight contract so status/context consumers can read SBOM risk without deep parsing.
7
+ - Surfaced SBOM risk in `context --json`, `AIENV.md`, and the dashboard Light SBOM card.
8
+ - Added recommended actions for read-only security scans and high light-SBOM risk review.
9
+ - Used top-risk package severity as a fallback when scanner summary severity counts are incomplete.
10
+ - Updated dependency change hints to point agents to `aienvmp checkpoint` after accepted dependency changes.
11
+ - Added regression tests for risk scoring, scanner-off guidance, status/context outputs, dashboard rendering, and recommended actions.
12
+
3
13
  ## 0.1.45
4
14
 
5
15
  - Added `aienvmp checkpoint` as a one-command post-environment-change flow for record, sync, status, and handoff.
package/README.md CHANGED
@@ -47,6 +47,7 @@ AIENV.md # Markdown env map for AI agents
47
47
  - `schema --json` prints the stable AI-readable output contract without scanning.
48
48
  - `status.json.nextAgent` tells the next AI what to read and whether to review first.
49
49
  - `dependencyReadSet` lists manifests and lockfiles before package or security changes.
50
+ - `sbomRisk` gives AI a compact light-SBOM risk level, signals, and next command.
50
51
  - `coordination.conflictTargets` shows where multiple agents are planning changes.
51
52
  - `agentActivity.multiActorTargets` shows where multiple agents actually recorded env changes.
52
53
  - `followUps` shows records that still need `sync`, `status`, or `handoff`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/actions.js CHANGED
@@ -20,6 +20,7 @@ export function recommendedActions(manifest = {}, context = {}) {
20
20
  }
21
21
 
22
22
  actions.push(...securityActions(manifest.security));
23
+ actions.push(...sbomRiskActions(manifest.lightSbom?.riskSummary));
23
24
 
24
25
  if (hasRuntimePolicyWarning(warnings)) {
25
26
  actions.push(action("review-version-policy", "medium", "runtime", "Detected runtime or package manager policy drift. Prefer project-local version files and ask before global changes."));
@@ -36,6 +37,18 @@ export function recommendedActions(manifest = {}, context = {}) {
36
37
  return dedupeActions(actions).slice(0, 8);
37
38
  }
38
39
 
40
+ function sbomRiskActions(risk = {}) {
41
+ const actions = [];
42
+ if (!risk.level) return actions;
43
+ if (risk.scanner === "off" && risk.vulnerabilityCount === 0) {
44
+ actions.push(action("scan-sbom-risk", "medium", "security", "Run a read-only security scan before dependency or release decisions.", "aienvmp sync --security"));
45
+ }
46
+ if (["urgent", "high"].includes(risk.level)) {
47
+ actions.push(action("review-sbom-risk", "high", "security", `Review light SBOM risk summary before dependency changes: ${(risk.signals || []).slice(0, 2).join("; ") || risk.level}.`, "aienvmp plan --write"));
48
+ }
49
+ return actions;
50
+ }
51
+
39
52
  function securityActions(security = {}) {
40
53
  if (!security.enabled) return [];
41
54
 
@@ -27,6 +27,7 @@ export async function contextWorkspace(args) {
27
27
  status: warnings.length ? "review-required" : "clear",
28
28
  preflight,
29
29
  coordination: preflight.coordination,
30
+ sbomRisk: preflight.sbomRisk,
30
31
  followUps: preflight.followUps,
31
32
  decision,
32
33
  enforcement: enforcementAdvice(warnings),
@@ -75,6 +76,7 @@ function lightSbomSummary(lightSbom = {}) {
75
76
  guidance: "No lockfile policy detected."
76
77
  },
77
78
  dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 8),
79
+ riskSummary: lightSbom.riskSummary || {},
78
80
  source: lightSbom.source || {},
79
81
  confidence: lightSbom.confidence || {},
80
82
  limitations: lightSbom.limitations || [],
package/src/contract.js CHANGED
@@ -4,7 +4,7 @@ export function preflightContract() {
4
4
  version: 1,
5
5
  stability: "additive",
6
6
  requiredFields: ["schemaVersion", "state", "decision", "quickstart", "commands", "artifacts"],
7
- aiEntryFields: ["state", "summary", "nextAgent", "coordination", "agentActivity", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
7
+ aiEntryFields: ["state", "summary", "nextAgent", "coordination", "agentActivity", "sbomRisk", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
8
8
  rule: "Consumers should ignore unknown fields and treat missing optional fields as unavailable."
9
9
  };
10
10
  }
@@ -108,6 +108,7 @@ export function buildLightSbom(snapshot = {}, security = {}) {
108
108
  transitiveOrUnmatchedVulnerablePackages: transitiveOrUnmatched.length
109
109
  },
110
110
  topRisk,
111
+ riskSummary: lightSbomRiskSummary({ packages, security, directVulnerable, transitiveOrUnmatched, topRisk, lockfiles: snapshot.lockfiles || [] }),
111
112
  packageManagerPolicy: packageManagerPolicy(snapshot.lockfiles || []),
112
113
  dependencyChangeHints: dependencyChangeHints(packages, topRisk, snapshot.lockfiles || []),
113
114
  aiUse: {
@@ -119,6 +120,74 @@ export function buildLightSbom(snapshot = {}, security = {}) {
119
120
  };
120
121
  }
121
122
 
123
+ export function lightSbomRiskSummary({ packages = [], security = {}, directVulnerable = [], transitiveOrUnmatched = [], topRisk = [], lockfiles = [] } = {}) {
124
+ const signals = [];
125
+ let score = 0;
126
+ const critical = Math.max(Number(security.summary?.critical || 0), topRisk.filter((pkg) => pkg.severity === "critical").length);
127
+ const high = Math.max(Number(security.summary?.high || 0), topRisk.filter((pkg) => pkg.severity === "high").length);
128
+ const total = Number(security.summary?.total || 0);
129
+ if (critical > 0) {
130
+ score += 45;
131
+ signals.push(`${critical} critical vulnerability finding(s)`);
132
+ }
133
+ if (high > 0) {
134
+ score += 30;
135
+ signals.push(`${high} high vulnerability finding(s)`);
136
+ }
137
+ if (directVulnerable.length) {
138
+ score += 15;
139
+ signals.push(`${directVulnerable.length} vulnerable direct dependency package(s)`);
140
+ }
141
+ if (transitiveOrUnmatched.length) {
142
+ score += 8;
143
+ signals.push(`${transitiveOrUnmatched.length} vulnerable transitive or unmatched package(s)`);
144
+ }
145
+ if (packageManagerPolicy(lockfiles).status === "review-required") {
146
+ score += 10;
147
+ signals.push("mixed package manager lockfiles");
148
+ }
149
+ if (packages.length && security.enabled !== true) {
150
+ score += 5;
151
+ signals.push("security scanner summary is off");
152
+ }
153
+ const level = score >= 90 ? "urgent" : score >= 60 ? "high" : score >= 30 ? "medium" : score > 0 ? "low" : "clear";
154
+ const reviewTargets = [
155
+ ...new Set([
156
+ ...topRisk.map((pkg) => pkg.manifest).filter(Boolean),
157
+ ...topRisk.map((pkg) => pkg.name).filter(Boolean).slice(0, 5)
158
+ ])
159
+ ].slice(0, 8);
160
+ return {
161
+ level,
162
+ score,
163
+ signals,
164
+ scanner: security.enabled ? "enabled" : "off",
165
+ vulnerabilityCount: total,
166
+ directVulnerablePackages: directVulnerable.length,
167
+ reviewTargets,
168
+ next: riskNext(level, security.enabled, topRisk),
169
+ commands: riskCommands(level, security.enabled)
170
+ };
171
+ }
172
+
173
+ function riskNext(level, securityEnabled, topRisk = []) {
174
+ if (!securityEnabled) return "Run read-only security scan before dependency or release decisions.";
175
+ if (level === "urgent" || level === "high") return "Review dependency read set and topRisk before remediation; do not auto-fix without user approval.";
176
+ if (topRisk.length) return "Review topRisk packages before dependency changes.";
177
+ return "No SBOM risk signal requires action in the lightweight snapshot.";
178
+ }
179
+
180
+ function riskCommands(level, securityEnabled) {
181
+ const commands = [];
182
+ if (!securityEnabled) commands.push("aienvmp sync --security");
183
+ if (["urgent", "high", "medium"].includes(level)) {
184
+ commands.push("aienvmp intent --actor agent:id --action dependency-review --target dependency");
185
+ commands.push("aienvmp plan --write");
186
+ }
187
+ commands.push("aienvmp checkpoint --actor agent:id --summary dependency-review --target dependency");
188
+ return commands;
189
+ }
190
+
122
191
  function packageManagerPolicy(lockfiles = []) {
123
192
  const byEcosystem = {};
124
193
  for (const lockfile of lockfiles) {
@@ -196,8 +265,7 @@ function dependencyChangeHints(packages = [], topRisk = [], lockfiles = []) {
196
265
  ],
197
266
  afterChange: [
198
267
  "Run project tests or the narrowest relevant validation.",
199
- "Run aienvmp sync.",
200
- "Record the dependency change with aienvmp record."
268
+ "Run aienvmp checkpoint --actor agent:id --summary dependency-change --target dependency."
201
269
  ]
202
270
  }));
203
271
  }
package/src/preflight.js CHANGED
@@ -16,6 +16,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
16
16
  const coordination = coordinationSummary(intents);
17
17
  const followUps = pendingFollowUps(timeline);
18
18
  const agentActivity = agentActivitySummary(timeline);
19
+ const sbomRisk = manifest.lightSbom?.riskSummary || {};
19
20
  return {
20
21
  schemaVersion: 1,
21
22
  contract: preflightContract(),
@@ -57,6 +58,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
57
58
  nextAgent: nextAgentHint(state, dependencyReadSet, dependencyChangeProtocol),
58
59
  coordination,
59
60
  agentActivity,
61
+ sbomRisk,
60
62
  followUps,
61
63
  intentTargets,
62
64
  dependencyReadSet,
package/src/render.js CHANGED
@@ -416,10 +416,12 @@ const lightSbom=manifest.lightSbom||{};
416
416
  const lightSbomSummary=lightSbom.summary||{};
417
417
  const pmPolicy=lightSbom.packageManagerPolicy||{};
418
418
  const topRisk=lightSbom.topRisk||[];
419
+ const riskSummary=lightSbom.riskSummary||{};
419
420
  const dependencyHints=lightSbom.dependencyChangeHints||[];
420
421
  const dependencyHintsHtml=dependencyHints.length?'<div class="timeline">'+dependencyHints.slice(0,5).map(h=>\`<div class="event"><time>\${esc(h.ecosystem||'deps')}</time><div><b>\${esc(h.manifest)}</b> <code>\${esc(h.manager||'unknown')}</code> \${esc(h.packages||0)} packages\${h.riskPackages?.length?\`<div class="path">risk: \${esc(h.riskPackages.map(p=>p.name).join(', '))}</div>\`:''}<div class="path">\${esc((h.groups||[]).join(', ')||'no groups')}\${h.lockfiles?.length?\` / lockfiles: \${esc(h.lockfiles.map(l=>l.file).join(', '))}\`:''}</div></div></div>\`).join('')+'</div>':'<div class="okline">No dependency change hints available.</div>';
421
422
  const pmPolicyHtml='<table><tr><th>Status</th><td><code>'+esc(pmPolicy.status||'no-lockfile')+'</code></td></tr><tr><th>Guidance</th><td>'+esc(pmPolicy.guidance||'No lockfile policy detected.')+'</td></tr></table>';
422
- const lightSbomHtml=\`<table><tr><th>Packages</th><td><code>\${esc(lightSbomSummary.packages||0)}</code></td></tr><tr><th>Vulnerabilities</th><td><code>\${esc(lightSbomSummary.vulnerabilities||0)}</code></td></tr><tr><th>Direct vulnerable</th><td><code>\${esc(lightSbomSummary.directVulnerablePackages||0)}</code></td></tr><tr><th>Manifests</th><td><code>\${esc((lightSbomSummary.manifests||[]).join(', ')||'none')}</code></td></tr><tr><th>Lockfiles</th><td><code>\${esc((lightSbomSummary.lockfiles||[]).map(l=>l.file).join(', ')||'none')}</code></td></tr></table><h3 style="margin-top:12px">Package manager policy</h3>\${pmPolicyHtml}\${topRisk.length?'<div class="timeline">'+topRisk.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.priority)}</time><div><b>\${esc(p.name)}</b> \${esc(p.severity)} \${p.directDependency?'<code>direct</code>':'<code>transitive</code>'}<div class="path">\${esc(p.manifest||p.ecosystem)} \${esc(p.version||'')}</div></div></div>\`).join('')+'</div>':'<div class="okline">No high-risk package summary in the current light SBOM.</div>'}<h3 style="margin-top:12px">Dependency change hints</h3>\${dependencyHintsHtml}\`;
423
+ const riskSummaryHtml=riskSummary.level?\`<table><tr><th>Level</th><td><code>\${esc(riskSummary.level)}</code> \${esc(riskSummary.score||0)}</td></tr><tr><th>Scanner</th><td><code>\${esc(riskSummary.scanner||'unknown')}</code></td></tr><tr><th>Next</th><td>\${esc(riskSummary.next||'No SBOM action required.')}</td></tr><tr><th>Targets</th><td>\${esc((riskSummary.reviewTargets||[]).join(', ')||'none')}</td></tr></table>\${riskSummary.signals?.length?'<div class="timeline">'+riskSummary.signals.slice(0,5).map(s=>\`<div class="event"><time>risk</time><div>\${esc(s)}</div></div>\`).join('')+'</div>':''}\`:'<div class="okline">No risk summary available.</div>';
424
+ const lightSbomHtml=\`<table><tr><th>Packages</th><td><code>\${esc(lightSbomSummary.packages||0)}</code></td></tr><tr><th>Vulnerabilities</th><td><code>\${esc(lightSbomSummary.vulnerabilities||0)}</code></td></tr><tr><th>Direct vulnerable</th><td><code>\${esc(lightSbomSummary.directVulnerablePackages||0)}</code></td></tr><tr><th>Manifests</th><td><code>\${esc((lightSbomSummary.manifests||[]).join(', ')||'none')}</code></td></tr><tr><th>Lockfiles</th><td><code>\${esc((lightSbomSummary.lockfiles||[]).map(l=>l.file).join(', ')||'none')}</code></td></tr></table><h3 style="margin-top:12px">Risk summary</h3>\${riskSummaryHtml}<h3 style="margin-top:12px">Package manager policy</h3>\${pmPolicyHtml}\${topRisk.length?'<div class="timeline">'+topRisk.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.priority)}</time><div><b>\${esc(p.name)}</b> \${esc(p.severity)} \${p.directDependency?'<code>direct</code>':'<code>transitive</code>'}<div class="path">\${esc(p.manifest||p.ecosystem)} \${esc(p.version||'')}</div></div></div>\`).join('')+'</div>':'<div class="okline">No high-risk package summary in the current light SBOM.</div>'}<h3 style="margin-top:12px">Dependency change hints</h3>\${dependencyHintsHtml}\`;
423
425
  const sec=manifest.security||{};
424
426
  const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
425
427
  const secPackages=sec.topPackages||[];
@@ -633,6 +635,10 @@ function lightSbomLines(lightSbom = {}) {
633
635
  lines.push(`- Source: ${lightSbom.source?.dependencies || "project manifests"}; vulnerabilities: ${lightSbom.source?.vulnerabilities || "not scanned"}`);
634
636
  lines.push(`- Confidence: direct ${lightSbom.confidence?.directDependencies || "unknown"}; transitive ${lightSbom.confidence?.transitiveDependencies || "unknown"}`);
635
637
  }
638
+ if (lightSbom.riskSummary) {
639
+ lines.push(`- Risk summary: ${lightSbom.riskSummary.level || "clear"}/${lightSbom.riskSummary.score || 0}; ${lightSbom.riskSummary.next || "no action"}`);
640
+ if (lightSbom.riskSummary.signals?.length) lines.push(`- Risk signals: ${lightSbom.riskSummary.signals.join("; ")}`);
641
+ }
636
642
  if (lightSbom.packageManagerPolicy) {
637
643
  lines.push(`- Package manager policy: ${lightSbom.packageManagerPolicy.status}`);
638
644
  lines.push(`- Package manager guidance: ${lightSbom.packageManagerPolicy.guidance}`);