aienvmp 0.1.6 → 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 +12 -0
- package/README.md +6 -2
- package/ROADMAP.md +6 -2
- package/package.json +1 -1
- package/src/cli.js +3 -3
- package/src/commands/context.js +9 -0
- package/src/commands/handoff.js +27 -1
- package/src/commands/scan.js +1 -1
- package/src/doctor.js +60 -0
- package/src/manifest.js +13 -3
- package/src/render.js +28 -4
- package/src/security.js +90 -0
- package/src/shell.js +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.1.7
|
|
10
|
+
|
|
11
|
+
- Added stale open intent warnings for long-running environment change plans.
|
|
12
|
+
- Added stale handoff warnings when environment changes happen after the last recorded AI handoff.
|
|
13
|
+
- Added optional `aienvmp handoff --record --actor <agent:id>` timeline entries.
|
|
14
|
+
|
|
3
15
|
## 0.1.6
|
|
4
16
|
|
|
5
17
|
- Added optional `sync --deep` / `scan --deep` read-only global tool inventory.
|
package/README.md
CHANGED
|
@@ -18,12 +18,14 @@ Core loop: scan the env, give AI a preflight context, and hand off safe next ste
|
|
|
18
18
|
npx aienvmp sync
|
|
19
19
|
npx aienvmp context
|
|
20
20
|
npx aienvmp handoff
|
|
21
|
+
npx aienvmp handoff --record --actor agent:codex
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
Optional
|
|
24
|
+
Optional read-only checks:
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
27
|
npx aienvmp sync --deep
|
|
28
|
+
npx aienvmp sync --security
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
## Output
|
|
@@ -65,7 +67,9 @@ aienvmp doctor --ci # strict CI check
|
|
|
65
67
|
- simple by default
|
|
66
68
|
- AI-first
|
|
67
69
|
- lightweight
|
|
68
|
-
-
|
|
70
|
+
- one advisory engine, optional enforcement with `doctor --ci`
|
|
71
|
+
- non-blocking unless strict mode is explicitly requested
|
|
72
|
+
- security checks are opt-in and read-only
|
|
69
73
|
|
|
70
74
|
## Development
|
|
71
75
|
|
package/ROADMAP.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
- Strengthen trust states: observed, planned, changed, review, verified, stale
|
|
8
8
|
- Detect multi-agent environment intent conflicts
|
|
9
|
+
- Keep one advisory decision engine with optional strict enforcement
|
|
10
|
+
- Keep vulnerability checks opt-in and read-only
|
|
9
11
|
- Stabilize `.aienvmp/manifest.json` and JSON command schemas
|
|
10
12
|
- Keep `sync`, `context`, and `handoff` as the simple core flow
|
|
11
13
|
- Improve the dashboard for 10-second human review
|
|
@@ -19,10 +21,12 @@
|
|
|
19
21
|
- Global tool inventory:
|
|
20
22
|
- richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
|
|
21
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
|
|
22
27
|
- Conflict detection:
|
|
23
|
-
- stale unresolved intents
|
|
24
|
-
- recent runtime changes without a fresh handoff
|
|
25
28
|
- package manager policy vs lockfile mismatch
|
|
29
|
+
- monorepo/project boundary aware intent targets
|
|
26
30
|
- CI mode:
|
|
27
31
|
- stable exit codes
|
|
28
32
|
- GitHub Action example
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -74,9 +74,9 @@ 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
|
-
aienvmp handoff [--dir .] [--json]
|
|
79
|
+
aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
|
|
80
80
|
|
|
81
81
|
Common:
|
|
82
82
|
aienvmp sync update AIENV.md, manifest, ledger, intents, and dashboard
|
|
@@ -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]
|
package/src/commands/context.js
CHANGED
|
@@ -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 {
|
package/src/commands/handoff.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { diagnose } from "../doctor.js";
|
|
2
|
-
import { readJson } from "../fsutil.js";
|
|
2
|
+
import { appendJsonLine, readJson } from "../fsutil.js";
|
|
3
3
|
import { loadPolicy, policyWarnings } from "../policy.js";
|
|
4
4
|
import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
|
|
5
5
|
import { renderHandoff } from "../render.js";
|
|
6
6
|
import { openIntents, readJsonl, readTimeline } from "../timeline.js";
|
|
7
|
+
import { changedTrust } from "../trust.js";
|
|
7
8
|
|
|
8
9
|
export async function handoffWorkspace(args) {
|
|
9
10
|
const dir = workspaceDir(args);
|
|
@@ -14,6 +15,9 @@ export async function handoffWorkspace(args) {
|
|
|
14
15
|
const policy = await loadPolicy(dir);
|
|
15
16
|
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
16
17
|
const handoff = buildHandoff(manifest, timeline, warnings, intents, policy);
|
|
18
|
+
if (args.record) {
|
|
19
|
+
await recordHandoff(timelinePath(dir), handoff, args.actor || "agent:unknown");
|
|
20
|
+
}
|
|
17
21
|
|
|
18
22
|
if (args.json) {
|
|
19
23
|
console.log(JSON.stringify(handoff, null, 2));
|
|
@@ -23,6 +27,19 @@ export async function handoffWorkspace(args) {
|
|
|
23
27
|
return handoff;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
async function recordHandoff(file, handoff, actor) {
|
|
31
|
+
const now = new Date();
|
|
32
|
+
await appendJsonLine(file, {
|
|
33
|
+
at: now.toISOString(),
|
|
34
|
+
actor,
|
|
35
|
+
type: "agent-handoff",
|
|
36
|
+
summary: `handoff ${handoff.status}`,
|
|
37
|
+
status: handoff.status,
|
|
38
|
+
warnings: handoff.warnings.map((warning) => warning.code),
|
|
39
|
+
trust: changedTrust(now, handoff.status !== "clear")
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
export function buildHandoff(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
|
|
27
44
|
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
28
45
|
return {
|
|
@@ -36,6 +53,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
36
53
|
docker: manifest.containers?.docker ? "available" : "not detected"
|
|
37
54
|
},
|
|
38
55
|
inventory: inventorySummary(manifest.inventory),
|
|
56
|
+
security: securitySummary(manifest.security),
|
|
39
57
|
policy: {
|
|
40
58
|
node: policy.node || "not set",
|
|
41
59
|
python: policy.python || "not set",
|
|
@@ -57,6 +75,14 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
57
75
|
};
|
|
58
76
|
}
|
|
59
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
|
+
|
|
60
86
|
function inventorySummary(inventory = {}) {
|
|
61
87
|
const tools = inventory.tools || {};
|
|
62
88
|
return {
|
package/src/commands/scan.js
CHANGED
|
@@ -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,10 +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 || []));
|
|
47
|
+
warnings.push(...staleIntentWarnings(context.intents || []));
|
|
48
|
+
warnings.push(...handoffWarnings(context.timeline || []));
|
|
46
49
|
return warnings;
|
|
47
50
|
}
|
|
48
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
|
+
|
|
49
64
|
export function coordinationWarnings(intents = []) {
|
|
50
65
|
const warnings = [];
|
|
51
66
|
const byTarget = new Map();
|
|
@@ -69,6 +84,30 @@ export function coordinationWarnings(intents = []) {
|
|
|
69
84
|
return warnings;
|
|
70
85
|
}
|
|
71
86
|
|
|
87
|
+
export function staleIntentWarnings(intents = [], now = new Date(), maxAgeHours = 4) {
|
|
88
|
+
const warnings = [];
|
|
89
|
+
for (const intent of intents) {
|
|
90
|
+
if (!isStaleTimestamp(intent.at, now, maxAgeHours)) continue;
|
|
91
|
+
warnings.push({
|
|
92
|
+
code: "stale-open-intent",
|
|
93
|
+
target: intent.target || inferTarget(intent.action) || "",
|
|
94
|
+
message: `Open intent ${intent.id || ""} from ${intent.actor || "unknown"} is older than ${maxAgeHours} hours. Resolve it or confirm it before environment changes.`.replace(/\s+/g, " ").trim()
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return warnings;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function handoffWarnings(timeline = []) {
|
|
101
|
+
const lastEnvChange = [...timeline].reverse().find(isEnvironmentChange);
|
|
102
|
+
if (!lastEnvChange) return [];
|
|
103
|
+
const lastHandoff = [...timeline].reverse().find((item) => item.type === "agent-handoff");
|
|
104
|
+
if (lastHandoff && new Date(lastHandoff.at).getTime() >= new Date(lastEnvChange.at).getTime()) return [];
|
|
105
|
+
return [{
|
|
106
|
+
code: "handoff-stale",
|
|
107
|
+
message: "Environment changes were recorded after the last AI handoff. Run `aienvmp handoff --record --actor agent:id` before the next agent continues."
|
|
108
|
+
}];
|
|
109
|
+
}
|
|
110
|
+
|
|
72
111
|
function inferTarget(action = "") {
|
|
73
112
|
const normalized = String(action).toLowerCase();
|
|
74
113
|
for (const target of ["node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
|
|
@@ -76,3 +115,24 @@ function inferTarget(action = "") {
|
|
|
76
115
|
}
|
|
77
116
|
return "";
|
|
78
117
|
}
|
|
118
|
+
|
|
119
|
+
function isEnvironmentChange(item = {}) {
|
|
120
|
+
if (["runtime", "package-manager", "container"].includes(item.change?.scope)) return true;
|
|
121
|
+
if (item.type === "detected-change") return false;
|
|
122
|
+
const text = `${item.type || ""} ${item.target || ""} ${item.summary || ""} ${item.action || ""} ${item.change?.scope || ""} ${item.change?.key || ""}`.toLowerCase();
|
|
123
|
+
return [
|
|
124
|
+
"runtime",
|
|
125
|
+
"node",
|
|
126
|
+
"python",
|
|
127
|
+
"docker",
|
|
128
|
+
"package manager",
|
|
129
|
+
"package-manager",
|
|
130
|
+
"npm",
|
|
131
|
+
"pnpm",
|
|
132
|
+
"yarn",
|
|
133
|
+
"uv",
|
|
134
|
+
"pip",
|
|
135
|
+
"pipx",
|
|
136
|
+
"global"
|
|
137
|
+
].some((token) => text.includes(token));
|
|
138
|
+
}
|
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
|
|
18
|
+
command: generatedCommand(options)
|
|
18
19
|
},
|
|
19
20
|
trust: observedTrust(new Date(now)),
|
|
20
21
|
workspace: {
|
|
@@ -27,13 +28,14 @@ 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",
|
|
33
35
|
preflightCommand: "aienvmp context",
|
|
34
36
|
handoffCommand: "aienvmp handoff",
|
|
35
|
-
intentCommand: "aienvmp intent --actor
|
|
36
|
-
recordCommand: "aienvmp record --actor
|
|
37
|
+
intentCommand: "aienvmp intent --actor agent:id --action planned-change",
|
|
38
|
+
recordCommand: "aienvmp record --actor agent:id --summary what-changed",
|
|
37
39
|
afterEnvironmentChange: ["aienvmp sync"],
|
|
38
40
|
trustModel: {
|
|
39
41
|
agentWritable: ["observed", "planned", "changed", "review", "stale"],
|
|
@@ -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
|
@@ -12,9 +12,10 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
12
12
|
lines.push("1. Run `aienvmp context`.");
|
|
13
13
|
lines.push("2. Prefer project-local version files such as `.nvmrc`, `.python-version`, `mise.toml`, and `.tool-versions`.");
|
|
14
14
|
lines.push("3. Ask the user before changing global environment state.");
|
|
15
|
-
lines.push("4. Record planned environment changes with `aienvmp intent --actor
|
|
15
|
+
lines.push("4. Record planned environment changes with `aienvmp intent --actor agent:id --action planned-change`.");
|
|
16
16
|
lines.push("5. After environment changes, run `aienvmp sync`.");
|
|
17
|
-
lines.push("6. Record what changed with `aienvmp record --actor
|
|
17
|
+
lines.push("6. Record what changed with `aienvmp record --actor agent:id --summary what-changed`.");
|
|
18
|
+
lines.push("7. At handoff, run `aienvmp handoff --record --actor agent:id`.", "");
|
|
18
19
|
lines.push("## Current Policy", "");
|
|
19
20
|
lines.push(...policyLines(policy));
|
|
20
21
|
lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
|
|
@@ -30,6 +31,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
30
31
|
pushMap(lines, "Containers", manifest.containers);
|
|
31
32
|
lines.push("## Global Tool Inventory", "");
|
|
32
33
|
lines.push(...inventoryLines(manifest.inventory), "");
|
|
34
|
+
lines.push("## Security Summary", "");
|
|
35
|
+
lines.push(...securityLines(manifest.security), "");
|
|
33
36
|
lines.push("## Project Requirements And Hints", "");
|
|
34
37
|
pushMap(lines, "Detected", manifest.projectHints);
|
|
35
38
|
lines.push("## Drift And Warnings", "");
|
|
@@ -76,6 +79,7 @@ Before changing runtimes, package managers, Docker settings, global packages, or
|
|
|
76
79
|
4. Record planned environment changes with \`aienvmp intent\`.
|
|
77
80
|
5. After environment changes, run \`aienvmp sync\`.
|
|
78
81
|
6. Record what changed with \`aienvmp record\`.
|
|
82
|
+
7. At handoff, run \`aienvmp handoff --record --actor agent:id\`.
|
|
79
83
|
|
|
80
84
|
\`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
|
|
81
85
|
}
|
|
@@ -94,6 +98,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
94
98
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
95
99
|
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
96
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"}`,
|
|
97
102
|
`Policy Node: ${policy.node || "not set"}`,
|
|
98
103
|
`Policy Python: ${policy.python || "not set"}`,
|
|
99
104
|
`Policy Package Manager: ${policy.packageManager || "not set"}`,
|
|
@@ -102,9 +107,10 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
102
107
|
"- Ask the user before global runtime, package manager, Docker, or global package changes.",
|
|
103
108
|
"- Treat policy mismatches as review-required, not as permission to break ongoing operations.",
|
|
104
109
|
"- Prefer project-local version files and local environments.",
|
|
105
|
-
"- Before planned env changes, run `aienvmp intent --actor
|
|
110
|
+
"- Before planned env changes, run `aienvmp intent --actor agent:id --action planned-change`.",
|
|
106
111
|
"- After env changes, run `aienvmp sync`.",
|
|
107
|
-
"- Then run `aienvmp record --actor
|
|
112
|
+
"- Then run `aienvmp record --actor agent:id --summary what-changed`.",
|
|
113
|
+
"- Before handing work to another AI, run `aienvmp handoff --record --actor agent:id`.",
|
|
108
114
|
"",
|
|
109
115
|
"Warnings:",
|
|
110
116
|
...(warnings.length ? warnings.map((w) => `- ${w.message}`) : ["- none"]),
|
|
@@ -132,6 +138,7 @@ export function renderHandoff(handoff) {
|
|
|
132
138
|
`- Python: ${handoff.safeRuntime.python}`,
|
|
133
139
|
`- Docker: ${handoff.safeRuntime.docker}`,
|
|
134
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"}`,
|
|
135
142
|
"",
|
|
136
143
|
"Open intents:",
|
|
137
144
|
...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
|
|
@@ -210,6 +217,9 @@ const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(
|
|
|
210
217
|
const inventoryGroups=manifest.inventory?.tools||{};
|
|
211
218
|
const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
|
|
212
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>';
|
|
213
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}\`;
|
|
214
224
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
215
225
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
@@ -254,6 +264,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
254
264
|
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
255
265
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
256
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)}
|
|
257
268
|
</div>
|
|
258
269
|
<aside>
|
|
259
270
|
\${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
|
|
@@ -323,6 +334,19 @@ function inventoryLines(inventory = {}) {
|
|
|
323
334
|
return lines;
|
|
324
335
|
}
|
|
325
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
|
+
|
|
326
350
|
function policyLines(policy) {
|
|
327
351
|
const lines = [];
|
|
328
352
|
if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
|
package/src/security.js
ADDED
|
@@ -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;
|