aienvmp 0.1.28 → 0.1.30

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.30
4
+
5
+ - Added shared enforcement advice for AI and CI surfaces.
6
+ - Exposed advisory-by-default behavior, suggested strict scopes, and scoped CI commands in context and plan outputs.
7
+ - Moved strict scope logic into a reusable enforcement module while keeping existing `doctor --strict` behavior compatible.
8
+
9
+ ## 0.1.29
10
+
11
+ - Added lightweight remediation priority scoring for vulnerable packages.
12
+ - Exposed priority level, score, and reasons in security summaries, plans, compact context, and dashboard rows.
13
+ - Kept scoring advisory-only so AI agents can choose safer next steps without blocking local operation.
14
+
3
15
  ## 0.1.28
4
16
 
5
17
  - Linked vulnerable package summaries to the dependency snapshot when the package is directly declared.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  It helps Codex, Claude, Gemini, and humans avoid silent Node, Python, package manager, dependency, Docker, and security drift.
12
12
 
13
- Core loop: scan once, link runtime/dependency/security context, give AI a shared decision contract, and hand off safe next steps.
13
+ Core loop: scan once, link runtime/dependency/security context, give AI a shared decision contract with advisory priorities, and hand off safe next steps.
14
14
 
15
15
  ## Quick Start
16
16
 
@@ -97,6 +97,7 @@ aienvmp doctor --strict security # fail only scoped warnings
97
97
  - read-only planning, no automatic fixes
98
98
  - one advisory engine, optional enforcement with `doctor --ci`
99
99
  - scoped enforcement with `doctor --strict security|policy|coordination|all`
100
+ - context and plan expose suggested strict scopes for CI
100
101
  - non-blocking unless strict mode is explicitly requested
101
102
  - security checks are opt-in and read-only
102
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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
@@ -42,7 +42,8 @@ function securityActions(security = {}) {
42
42
 
43
43
  const packageHints = packages.map((pkg) => {
44
44
  const fix = pkg.fixVersions?.length ? `fix ${pkg.fixVersions.slice(0, 2).join(", ")}` : pkg.fixAvailable ? "fix available" : "review required";
45
- return `${pkg.name} (${pkg.severity}, ${fix})`;
45
+ const priority = pkg.remediationPriority ? `${pkg.remediationPriority.level}/${pkg.remediationPriority.score}, ` : "";
46
+ return `${pkg.name} (${priority}${pkg.severity}, ${fix})`;
46
47
  });
47
48
 
48
49
  return [action(
@@ -7,6 +7,7 @@ import { loadPolicy, policyWarnings } from "../policy.js";
7
7
  import { recommendedActions } from "../actions.js";
8
8
  import { buildPlan, compactStepSummary } from "./plan.js";
9
9
  import { aiDecision } from "../decision.js";
10
+ import { enforcementAdvice } from "../enforcement.js";
10
11
 
11
12
  export async function contextWorkspace(args) {
12
13
  const dir = workspaceDir(args);
@@ -23,6 +24,7 @@ export async function contextWorkspace(args) {
23
24
  console.log(JSON.stringify({
24
25
  status: warnings.length ? "review-required" : "clear",
25
26
  decision,
27
+ enforcement: enforcementAdvice(warnings),
26
28
  recommendedActions: actions,
27
29
  stepSummary,
28
30
  trust: manifest.trust || {},
@@ -8,7 +8,7 @@ import { dashboardPath, intentsPath, manifestPath, planJsonPath, planMdPath, tim
8
8
  import { renderDashboard } from "../render.js";
9
9
  import { loadPolicy, policyWarnings } from "../policy.js";
10
10
  import { recommendedActions } from "../actions.js";
11
- import { strictResult } from "./doctor.js";
11
+ import { strictResult } from "../enforcement.js";
12
12
 
13
13
  export async function dashWorkspace(args) {
14
14
  const dir = workspaceDir(args);
@@ -4,6 +4,9 @@ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
4
4
  import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
5
5
  import { loadPolicy, policyWarnings } from "../policy.js";
6
6
  import { recommendedActions } from "../actions.js";
7
+ import { enforcementAdvice, strictResult } from "../enforcement.js";
8
+
9
+ export { strictResult } from "../enforcement.js";
7
10
 
8
11
  export async function doctorWorkspace(args) {
9
12
  const dir = workspaceDir(args);
@@ -23,6 +26,7 @@ export async function doctorWorkspace(args) {
23
26
  openIntentCount: intents.length,
24
27
  warnings,
25
28
  recommendedActions: actions,
29
+ enforcement: enforcementAdvice(warnings),
26
30
  strict
27
31
  }, null, 2));
28
32
  if (strict.fail) {
@@ -46,34 +50,3 @@ export async function doctorWorkspace(args) {
46
50
  process.exitCode = 1;
47
51
  }
48
52
  }
49
-
50
- export function strictResult(warnings = [], args = {}) {
51
- const scope = normalizeStrictScope(args.strict || (args.ci ? "all" : ""));
52
- const matchedWarnings = scope ? warnings.filter((warning) => warningMatchesScope(warning, scope)) : [];
53
- return {
54
- enabled: Boolean(scope),
55
- scope: scope || "off",
56
- fail: matchedWarnings.length > 0,
57
- matchedWarningCodes: matchedWarnings.map((warning) => warning.code),
58
- availableScopes: ["security", "policy", "coordination", "all"]
59
- };
60
- }
61
-
62
- function normalizeStrictScope(value) {
63
- if (value === true) return "all";
64
- const scope = String(value || "").trim().toLowerCase();
65
- if (!scope || scope === "false" || scope === "off") return "";
66
- if (["security", "policy", "coordination", "all"].includes(scope)) return scope;
67
- return "all";
68
- }
69
-
70
- function warningMatchesScope(warning, scope) {
71
- if (scope === "all") return true;
72
- return warningScope(warning.code) === scope;
73
- }
74
-
75
- function warningScope(code = "") {
76
- if (code === "security-vulnerabilities") return "security";
77
- if (["conflicting-open-intents", "stale-open-intent", "handoff-stale"].includes(code)) return "coordination";
78
- return "policy";
79
- }
@@ -7,6 +7,7 @@ import { renderPlan } from "../render.js";
7
7
  import { recommendedActions } from "../actions.js";
8
8
  import { openIntents, readJsonl, readTimeline } from "../timeline.js";
9
9
  import { aiDecision } from "../decision.js";
10
+ import { enforcementAdvice } from "../enforcement.js";
10
11
 
11
12
  export async function planWorkspace(args) {
12
13
  const dir = workspaceDir(args);
@@ -41,6 +42,7 @@ export function buildPlan(manifest, warnings = [], intents = [], policy = {}) {
41
42
  workspace: manifest.workspace || {},
42
43
  trust: manifest.trust || {},
43
44
  decision: aiDecision(warnings, intents),
45
+ enforcement: enforcementAdvice(warnings),
44
46
  policy: {
45
47
  node: policy.node || "not set",
46
48
  python: policy.python || "not set",
@@ -73,6 +75,8 @@ export function compactStepSummary(plan = {}) {
73
75
  remediation: (plan.remediationSteps || []).slice(0, 3).map((item) => ({
74
76
  package: item.package,
75
77
  severity: item.severity,
78
+ priority: item.remediationPriority?.level || "low",
79
+ score: item.remediationPriority?.score || 0,
76
80
  fixVersions: (item.fixVersions || []).slice(0, 3),
77
81
  advisoryIds: (item.advisories || []).map((advisory) => advisory.id || advisory.title).filter(Boolean).slice(0, 3)
78
82
  })),
@@ -175,6 +179,7 @@ function remediationSteps(security = {}) {
175
179
  package: pkg.name,
176
180
  scanner: pkg.scanner || "unknown",
177
181
  severity: pkg.severity || "unknown",
182
+ remediationPriority: pkg.remediationPriority || { level: "low", score: 0, reasons: [] },
178
183
  directDependency: pkg.directDependency === true,
179
184
  dependency: pkg.dependency || null,
180
185
  fixAvailable: pkg.fixAvailable === true,
@@ -29,20 +29,43 @@ export function linkVulnerableDependencies(security = {}, snapshot = {}) {
29
29
  ...security,
30
30
  topPackages: (security.topPackages || []).map((pkg) => {
31
31
  const dependency = dependencyIndex.get(dependencyKey(scannerEcosystem(pkg.scanner), pkg.name));
32
+ const directDependency = Boolean(dependency);
32
33
  return {
33
34
  ...pkg,
34
- directDependency: Boolean(dependency),
35
+ directDependency,
35
36
  dependency: dependency ? {
36
37
  ecosystem: dependency.ecosystem,
37
38
  manifest: dependency.manifest,
38
39
  group: dependency.group,
39
40
  version: dependency.version
40
- } : null
41
+ } : null,
42
+ remediationPriority: remediationPriority(pkg, { directDependency })
41
43
  };
42
44
  })
43
45
  };
44
46
  }
45
47
 
48
+ export function remediationPriority(pkg = {}, context = {}) {
49
+ const severityScore = { critical: 90, high: 70, moderate: 45, low: 20, info: 5, unknown: 30 };
50
+ const severity = String(pkg.severity || "unknown").toLowerCase();
51
+ const reasons = [`severity:${severity}`];
52
+ let score = severityScore[severity] ?? severityScore.unknown;
53
+ if (context.directDependency) {
54
+ score += 15;
55
+ reasons.push("direct-dependency");
56
+ } else {
57
+ reasons.push("not-direct-in-snapshot");
58
+ }
59
+ if (pkg.fixAvailable === true || (Array.isArray(pkg.fixVersions) && pkg.fixVersions.length)) {
60
+ score += 5;
61
+ reasons.push("fix-available");
62
+ } else {
63
+ reasons.push("fix-review-needed");
64
+ }
65
+ const level = score >= 95 ? "urgent" : score >= 75 ? "high" : score >= 50 ? "medium" : "low";
66
+ return { level, score, reasons };
67
+ }
68
+
46
69
  async function scanNodeDependencies(dir) {
47
70
  const file = path.join(dir, "package.json");
48
71
  if (!(await exists(file))) return { manifests: [], packages: [] };
@@ -0,0 +1,57 @@
1
+ const STRICT_SCOPES = ["security", "policy", "coordination", "all"];
2
+
3
+ export function strictResult(warnings = [], args = {}) {
4
+ const scope = normalizeStrictScope(args.strict || (args.ci ? "all" : ""));
5
+ const matchedWarnings = scope ? warnings.filter((warning) => warningMatchesScope(warning, scope)) : [];
6
+ return {
7
+ enabled: Boolean(scope),
8
+ scope: scope || "off",
9
+ fail: matchedWarnings.length > 0,
10
+ matchedWarningCodes: matchedWarnings.map((warning) => warning.code),
11
+ availableScopes: STRICT_SCOPES
12
+ };
13
+ }
14
+
15
+ export function enforcementAdvice(warnings = []) {
16
+ const scopeResults = STRICT_SCOPES.map((scope) => {
17
+ const result = strictResult(warnings, { strict: scope });
18
+ return {
19
+ scope,
20
+ status: result.fail ? "fail" : "pass",
21
+ matchedWarningCodes: result.matchedWarningCodes
22
+ };
23
+ });
24
+ const suggestedStrictScopes = scopeResults
25
+ .filter((item) => item.scope !== "all" && item.status === "fail")
26
+ .map((item) => item.scope);
27
+ return {
28
+ mode: "advisory-by-default",
29
+ localBehavior: "non-blocking",
30
+ ciBehavior: "strict-only-when-requested",
31
+ suggestedStrictScopes,
32
+ scopes: scopeResults,
33
+ recommendedCommand: suggestedStrictScopes.length
34
+ ? `aienvmp doctor --strict ${suggestedStrictScopes[0]}`
35
+ : "aienvmp doctor --strict all",
36
+ note: "Use strict mode in CI or explicit checks; do not block local operation unless the user requests it."
37
+ };
38
+ }
39
+
40
+ function normalizeStrictScope(value) {
41
+ if (value === true) return "all";
42
+ const scope = String(value || "").trim().toLowerCase();
43
+ if (!scope || scope === "false" || scope === "off") return "";
44
+ if (STRICT_SCOPES.includes(scope)) return scope;
45
+ return "all";
46
+ }
47
+
48
+ function warningMatchesScope(warning, scope) {
49
+ if (scope === "all") return true;
50
+ return warningScope(warning.code) === scope;
51
+ }
52
+
53
+ function warningScope(code = "") {
54
+ if (code === "security-vulnerabilities") return "security";
55
+ if (["conflicting-open-intents", "stale-open-intent", "handoff-stale"].includes(code)) return "coordination";
56
+ return "policy";
57
+ }
package/src/render.js CHANGED
@@ -95,6 +95,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
95
95
  `Status: ${status}`,
96
96
  `Next: ${next}`,
97
97
  "Project-local work: allowed; environment changes require intent and review when warnings or open intents exist.",
98
+ "Enforcement: advisory by default; use `aienvmp doctor --strict <scope>` only when explicit CI failure is wanted.",
98
99
  `Trust: ${manifest.trust?.state || "observed"} (verified requires human or CI)`,
99
100
  `Workspace: ${manifest.workspace.path}`,
100
101
  `Node: ${manifest.runtimes.node || "not detected"}`,
@@ -175,6 +176,7 @@ export function renderPlan(plan) {
175
176
  "",
176
177
  `Status: ${plan.status}`,
177
178
  `Decision: ${plan.decision?.mode || plan.status}`,
179
+ `Enforcement: ${plan.enforcement?.mode || "advisory-by-default"} (${plan.enforcement?.localBehavior || "non-blocking"})`,
178
180
  `Generated: ${plan.generatedAt}`,
179
181
  `Workspace: ${plan.workspace?.path || "unknown"}`,
180
182
  "",
@@ -212,8 +214,9 @@ function remediationLines(item) {
212
214
  const fix = item.fixVersions?.length ? `fix ${item.fixVersions.join(", ")}` : item.fixAvailable ? "fix available" : "review required";
213
215
  const advisories = (item.advisories || []).map((advisory) => advisory.id || advisory.title).filter(Boolean).slice(0, 2);
214
216
  const dependency = item.directDependency && item.dependency ? `; declared in ${item.dependency.manifest} ${item.dependency.version}` : "; not found in dependency snapshot";
217
+ const priority = item.remediationPriority ? `priority ${item.remediationPriority.level}/${item.remediationPriority.score}` : "priority unscored";
215
218
  return [
216
- `- ${item.package}: ${item.severity}; ${fix}${dependency}${advisories.length ? `; advisories ${advisories.join(", ")}` : ""}`,
219
+ `- ${item.package}: ${item.severity}; ${priority}; ${fix}${dependency}${advisories.length ? `; advisories ${advisories.join(", ")}` : ""}`,
217
220
  ...item.steps.slice(0, 4).map((step) => ` - ${step}`)
218
221
  ];
219
222
  }
@@ -287,7 +290,8 @@ const secPackages=sec.topPackages||[];
287
290
  const securityFix=p=>p.fixVersions?.length?\`fix \${p.fixVersions.slice(0,3).join(', ')}\`:(p.fixAvailable?'fix available':'review required');
288
291
  const securityRefs=p=>p.advisories?.length?\` - \${p.advisories.map(a=>a.id||a.title).filter(Boolean).slice(0,2).join(', ')}\`:'';
289
292
  const securityDep=p=>p.directDependency&&p.dependency?\`<div class="path">\${esc(p.dependency.manifest)} / \${esc(p.dependency.group)} / \${esc(p.dependency.version)}</div>\`:'<div class="path">not found in dependency snapshot</div>';
290
- 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>\${secPackages.length?'<div class="timeline">'+secPackages.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.severity)}</time><div><b>\${esc(p.name)}</b> \${esc(securityFix(p))}\${esc(securityRefs(p))}\${securityDep(p)}</div></div>\`).join('')+'</div>':'<div class="okline" style="margin-top:10px">No vulnerable packages reported.</div>'}\`:'<div class="okline">Security scan is off. Run <code>aienvmp sync --security</code> for read-only vulnerability summary.</div>';
293
+ const securityPriority=p=>p.remediationPriority?\`<code>\${esc(p.remediationPriority.level)} \${esc(p.remediationPriority.score)}</code> \`:'';
294
+ 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>\${secPackages.length?'<div class="timeline">'+secPackages.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.severity)}</time><div><b>\${esc(p.name)}</b> \${securityPriority(p)}\${esc(securityFix(p))}\${esc(securityRefs(p))}\${securityDep(p)}</div></div>\`).join('')+'</div>':'<div class="okline" style="margin-top:10px">No vulnerable packages reported.</div>'}\`:'<div class="okline">Security scan is off. Run <code>aienvmp sync --security</code> for read-only vulnerability summary.</div>';
291
295
  const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
292
296
  const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
293
297
  const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
@@ -465,12 +469,13 @@ function securityLines(security = {}) {
465
469
 
466
470
  function securityPackageNote(pkg) {
467
471
  const fix = pkg.fixVersions?.length ? `fix ${pkg.fixVersions.slice(0, 3).join(", ")}` : pkg.fixAvailable ? "fix available" : "review required";
472
+ const priority = pkg.remediationPriority ? `priority ${pkg.remediationPriority.level}/${pkg.remediationPriority.score}; ` : "";
468
473
  const dependency = pkg.directDependency && pkg.dependency ? `; declared in ${pkg.dependency.manifest} ${pkg.dependency.version}` : "; not found in dependency snapshot";
469
474
  const advisories = (pkg.advisories || [])
470
475
  .map((item) => item.id || item.title)
471
476
  .filter(Boolean)
472
477
  .slice(0, 2);
473
- return advisories.length ? `${fix}${dependency}; advisories ${advisories.join(", ")}` : `${fix}${dependency}`;
478
+ return advisories.length ? `${priority}${fix}${dependency}; advisories ${advisories.join(", ")}` : `${priority}${fix}${dependency}`;
474
479
  }
475
480
 
476
481
  function policyLines(policy) {