aienvmp 0.1.4 → 0.1.6
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 +13 -0
- package/README.md +14 -8
- package/ROADMAP.md +8 -9
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/commands/compile.js +1 -1
- package/src/commands/context.js +12 -1
- package/src/commands/dash.js +1 -1
- package/src/commands/doctor.js +7 -2
- package/src/commands/handoff.js +13 -1
- package/src/commands/intent.js +5 -2
- package/src/commands/record.js +6 -2
- package/src/commands/scan.js +1 -1
- package/src/doctor.js +41 -1
- package/src/inventory.js +88 -0
- package/src/manifest.js +19 -2
- package/src/render.js +31 -4
- package/src/shell.js +3 -2
- package/src/trust.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
- Added optional `sync --deep` / `scan --deep` read-only global tool inventory.
|
|
6
|
+
- Kept the default scan lightweight while exposing deep inventory summaries to `AIENV.md`, `context`, `handoff`, and the dashboard.
|
|
7
|
+
- Added parsers for npm global packages, pipx tools, uv tools, and Homebrew package versions.
|
|
8
|
+
|
|
9
|
+
## 0.1.5
|
|
10
|
+
|
|
11
|
+
- Added machine-readable trust states for observed, planned, changed, review, verified, and stale environment facts.
|
|
12
|
+
- Added multi-agent intent conflict warnings for shared runtime/package manager targets.
|
|
13
|
+
- Added trust and schema context to `context`, `handoff`, `doctor`, `AIENV.md`, and the dashboard.
|
|
14
|
+
- Repositioned the docs around AI environment coordination instead of general AI project memory.
|
|
15
|
+
|
|
3
16
|
## 0.1.4
|
|
4
17
|
|
|
5
18
|
- Added `aienvmp handoff` for next-agent environment handoff summaries.
|
package/README.md
CHANGED
|
@@ -6,22 +6,24 @@
|
|
|
6
6
|
|
|
7
7
|
**AI Environment Map.**
|
|
8
8
|
|
|
9
|
-
`aienvmp`
|
|
9
|
+
`aienvmp` is an AI environment coordination tool for shared coding machines.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- package managers
|
|
13
|
-
- Docker state
|
|
14
|
-
- project version hints
|
|
15
|
-
- planned env changes
|
|
16
|
-
- recent env changes
|
|
11
|
+
It helps Codex, Claude, Gemini, and other agents avoid silently using or installing different runtime and tool versions.
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
Core loop: scan the env, give AI a preflight context, and hand off safe next steps to the next agent.
|
|
19
14
|
|
|
20
15
|
## Use
|
|
21
16
|
|
|
22
17
|
```bash
|
|
23
18
|
npx aienvmp sync
|
|
24
19
|
npx aienvmp context
|
|
20
|
+
npx aienvmp handoff
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Optional deeper read-only inventory:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx aienvmp sync --deep
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
## Output
|
|
@@ -34,6 +36,10 @@ AIENV.md
|
|
|
34
36
|
.aienvmp/dashboard.html
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
|
|
40
|
+
|
|
41
|
+
AI agents can observe, plan, and record. Only a human or CI should mark environment facts as verified.
|
|
42
|
+
|
|
37
43
|
## For AGENTS.md
|
|
38
44
|
|
|
39
45
|
`aienvmp` does not replace AGENTS.md. It gives AGENTS.md a live environment source of truth.
|
package/ROADMAP.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Near Term
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- Strengthen trust states: observed, planned, changed, review, verified, stale
|
|
8
|
+
- Detect multi-agent environment intent conflicts
|
|
9
|
+
- Stabilize `.aienvmp/manifest.json` and JSON command schemas
|
|
10
|
+
- Keep `sync`, `context`, and `handoff` as the simple core flow
|
|
11
|
+
- Improve the dashboard for 10-second human review
|
|
12
12
|
|
|
13
13
|
## Next
|
|
14
14
|
|
|
@@ -17,12 +17,11 @@
|
|
|
17
17
|
- pyenv, uv, conda
|
|
18
18
|
- mise, asdf
|
|
19
19
|
- Global tool inventory:
|
|
20
|
-
- `npm -g`
|
|
21
|
-
- `
|
|
22
|
-
- `uv tool list`
|
|
20
|
+
- richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
|
|
21
|
+
- optional `--deep` scanners for more toolchains
|
|
23
22
|
- Conflict detection:
|
|
24
|
-
- multiple open intents for the same target
|
|
25
23
|
- stale unresolved intents
|
|
24
|
+
- recent runtime changes without a fresh handoff
|
|
26
25
|
- package manager policy vs lockfile mismatch
|
|
27
26
|
- CI mode:
|
|
28
27
|
- stable exit codes
|
package/package.json
CHANGED
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]
|
|
77
|
+
aienvmp sync [--dir .] [--json] [--quiet] [--deep]
|
|
78
78
|
aienvmp context [--dir .] [--json]
|
|
79
79
|
aienvmp handoff [--dir .] [--json]
|
|
80
80
|
|
|
@@ -87,7 +87,7 @@ Common:
|
|
|
87
87
|
|
|
88
88
|
Advanced:
|
|
89
89
|
aienvmp init [--dir .]
|
|
90
|
-
aienvmp scan [--dir .]
|
|
90
|
+
aienvmp scan [--dir .] [--deep]
|
|
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/compile.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function compileWorkspace(args) {
|
|
|
13
13
|
const timeline = await readTimeline(timelinePath(dir));
|
|
14
14
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
15
15
|
const policy = await loadPolicy(dir);
|
|
16
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
16
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
17
17
|
const rendered = renderAIEnv(manifest, timeline, warnings, intents, policy);
|
|
18
18
|
await fs.writeFile(aiEnvPath(dir), rendered, "utf8");
|
|
19
19
|
if (!args.quiet) {
|
package/src/commands/context.js
CHANGED
|
@@ -12,17 +12,19 @@ export async function contextWorkspace(args) {
|
|
|
12
12
|
const timeline = await readTimeline(timelinePath(dir));
|
|
13
13
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
14
14
|
const policy = await loadPolicy(dir);
|
|
15
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
15
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
16
16
|
const decision = contextDecision(warnings, intents);
|
|
17
17
|
if (args.json) {
|
|
18
18
|
console.log(JSON.stringify({
|
|
19
19
|
status: warnings.length ? "review-required" : "clear",
|
|
20
20
|
decision,
|
|
21
|
+
trust: manifest.trust || {},
|
|
21
22
|
guidance: decision,
|
|
22
23
|
workspace: manifest.workspace,
|
|
23
24
|
runtimes: manifest.runtimes,
|
|
24
25
|
packageManagers: manifest.packageManagers,
|
|
25
26
|
containers: manifest.containers,
|
|
27
|
+
inventory: inventorySummary(manifest.inventory),
|
|
26
28
|
projectHints: manifest.projectHints,
|
|
27
29
|
warnings,
|
|
28
30
|
policy,
|
|
@@ -35,6 +37,15 @@ export async function contextWorkspace(args) {
|
|
|
35
37
|
console.log(renderContext(manifest, timeline, warnings, intents, policy));
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function inventorySummary(inventory = {}) {
|
|
41
|
+
const tools = inventory.tools || {};
|
|
42
|
+
return {
|
|
43
|
+
mode: inventory.mode || "basic",
|
|
44
|
+
enabled: inventory.enabled === true,
|
|
45
|
+
groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
function contextDecision(warnings, intents) {
|
|
39
50
|
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
40
51
|
return {
|
package/src/commands/dash.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function dashWorkspace(args) {
|
|
|
15
15
|
const timeline = await readTimeline(timelinePath(dir));
|
|
16
16
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
17
17
|
const policy = await loadPolicy(dir);
|
|
18
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
18
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
19
19
|
const html = renderDashboard(manifest, timeline, warnings, intents, policy);
|
|
20
20
|
const out = dashboardPath(dir);
|
|
21
21
|
await fs.mkdir(path.dirname(out), { recursive: true });
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { diagnose } from "../doctor.js";
|
|
2
2
|
import { readJson } from "../fsutil.js";
|
|
3
|
-
import {
|
|
3
|
+
import { openIntents, readJsonl, readTimeline } from "../timeline.js";
|
|
4
|
+
import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
|
|
4
5
|
import { loadPolicy, policyWarnings } from "../policy.js";
|
|
5
6
|
|
|
6
7
|
export async function doctorWorkspace(args) {
|
|
@@ -8,11 +9,15 @@ export async function doctorWorkspace(args) {
|
|
|
8
9
|
const manifest = await readJson(manifestPath(dir));
|
|
9
10
|
if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
|
|
10
11
|
const policy = await loadPolicy(dir);
|
|
11
|
-
const
|
|
12
|
+
const timeline = await readTimeline(timelinePath(dir));
|
|
13
|
+
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
14
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
12
15
|
if (args.json) {
|
|
13
16
|
console.log(JSON.stringify({
|
|
14
17
|
status: warnings.length ? "warning" : "ok",
|
|
18
|
+
trust: manifest.trust || {},
|
|
15
19
|
policy,
|
|
20
|
+
openIntentCount: intents.length,
|
|
16
21
|
warnings
|
|
17
22
|
}, null, 2));
|
|
18
23
|
if (args.ci && warnings.length) {
|
package/src/commands/handoff.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function handoffWorkspace(args) {
|
|
|
12
12
|
const timeline = await readTimeline(timelinePath(dir));
|
|
13
13
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
14
14
|
const policy = await loadPolicy(dir);
|
|
15
|
-
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
15
|
+
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
16
16
|
const handoff = buildHandoff(manifest, timeline, warnings, intents, policy);
|
|
17
17
|
|
|
18
18
|
if (args.json) {
|
|
@@ -27,12 +27,15 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
27
27
|
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
28
28
|
return {
|
|
29
29
|
status: reviewRequired ? "review-required" : "clear",
|
|
30
|
+
trust: manifest.trust || {},
|
|
31
|
+
schemaVersion: manifest.schemaVersion || 1,
|
|
30
32
|
workspace: manifest.workspace,
|
|
31
33
|
safeRuntime: {
|
|
32
34
|
node: manifest.runtimes?.node || "not detected",
|
|
33
35
|
python: manifest.runtimes?.python || manifest.runtimes?.python3 || "not detected",
|
|
34
36
|
docker: manifest.containers?.docker ? "available" : "not detected"
|
|
35
37
|
},
|
|
38
|
+
inventory: inventorySummary(manifest.inventory),
|
|
36
39
|
policy: {
|
|
37
40
|
node: policy.node || "not set",
|
|
38
41
|
python: policy.python || "not set",
|
|
@@ -53,3 +56,12 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
53
56
|
: "continue with project-local work; record intent before environment changes"
|
|
54
57
|
};
|
|
55
58
|
}
|
|
59
|
+
|
|
60
|
+
function inventorySummary(inventory = {}) {
|
|
61
|
+
const tools = inventory.tools || {};
|
|
62
|
+
return {
|
|
63
|
+
mode: inventory.mode || "basic",
|
|
64
|
+
enabled: inventory.enabled === true,
|
|
65
|
+
groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
|
|
66
|
+
};
|
|
67
|
+
}
|
package/src/commands/intent.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { appendJsonLine } from "../fsutil.js";
|
|
2
2
|
import { intentsPath, workspaceDir } from "../paths.js";
|
|
3
3
|
import { newIntentID } from "../timeline.js";
|
|
4
|
+
import { plannedTrust } from "../trust.js";
|
|
4
5
|
|
|
5
6
|
export async function intentWorkspace(args) {
|
|
6
7
|
const dir = workspaceDir(args);
|
|
7
8
|
const actor = required(args.actor, "actor");
|
|
8
9
|
const action = required(args.action, "action");
|
|
10
|
+
const now = new Date();
|
|
9
11
|
const entry = {
|
|
10
|
-
at:
|
|
12
|
+
at: now.toISOString(),
|
|
11
13
|
type: "intent",
|
|
12
14
|
actor,
|
|
13
15
|
action,
|
|
14
16
|
target: args.target || "",
|
|
15
17
|
reason: args.reason || "",
|
|
16
|
-
status: "open"
|
|
18
|
+
status: "open",
|
|
19
|
+
trust: plannedTrust(now)
|
|
17
20
|
};
|
|
18
21
|
entry.id = newIntentID();
|
|
19
22
|
await appendJsonLine(intentsPath(dir), entry);
|
package/src/commands/record.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { appendJsonLine } from "../fsutil.js";
|
|
2
2
|
import { timelinePath, workspaceDir } from "../paths.js";
|
|
3
|
+
import { changedTrust } from "../trust.js";
|
|
3
4
|
|
|
4
5
|
export async function recordWorkspace(args) {
|
|
5
6
|
const dir = workspaceDir(args);
|
|
6
7
|
const actor = required(args.actor, "actor");
|
|
7
8
|
const summary = required(args.summary || args.change, "summary");
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const requiresReview = args.review === true || args.review === "true";
|
|
8
11
|
const entry = {
|
|
9
|
-
at:
|
|
12
|
+
at: now.toISOString(),
|
|
10
13
|
actor,
|
|
11
14
|
type: args.type || "agent-record",
|
|
12
15
|
summary,
|
|
@@ -14,7 +17,8 @@ export async function recordWorkspace(args) {
|
|
|
14
17
|
before: args.before || "",
|
|
15
18
|
after: args.after || "",
|
|
16
19
|
evidence: args.evidence || "",
|
|
17
|
-
requiresReview
|
|
20
|
+
requiresReview,
|
|
21
|
+
trust: changedTrust(now, requiresReview)
|
|
18
22
|
};
|
|
19
23
|
await appendJsonLine(timelinePath(dir), entry);
|
|
20
24
|
console.log(`recorded ${entry.type} by ${actor}`);
|
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);
|
|
14
|
+
const manifest = await buildManifest(dir, { deep: args.deep });
|
|
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
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { isStaleTimestamp } from "./trust.js";
|
|
2
|
+
|
|
3
|
+
export function diagnose(manifest, context = {}) {
|
|
2
4
|
const warnings = [];
|
|
3
5
|
const hints = manifest.projectHints || {};
|
|
4
6
|
const runtimes = manifest.runtimes || {};
|
|
@@ -34,5 +36,43 @@ export function diagnose(manifest) {
|
|
|
34
36
|
message: "Dockerfile detected, but Docker CLI was not found."
|
|
35
37
|
});
|
|
36
38
|
}
|
|
39
|
+
if (isStaleTimestamp(manifest.generatedAt)) {
|
|
40
|
+
warnings.push({
|
|
41
|
+
code: "manifest-stale",
|
|
42
|
+
message: "Environment snapshot is older than 24 hours. Run `aienvmp sync` before changing the environment."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
warnings.push(...coordinationWarnings(context.intents || []));
|
|
46
|
+
return warnings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function coordinationWarnings(intents = []) {
|
|
50
|
+
const warnings = [];
|
|
51
|
+
const byTarget = new Map();
|
|
52
|
+
for (const intent of intents) {
|
|
53
|
+
const target = String(intent.target || inferTarget(intent.action) || "").trim().toLowerCase();
|
|
54
|
+
if (!target) continue;
|
|
55
|
+
const list = byTarget.get(target) || [];
|
|
56
|
+
list.push(intent);
|
|
57
|
+
byTarget.set(target, list);
|
|
58
|
+
}
|
|
59
|
+
for (const [target, list] of byTarget) {
|
|
60
|
+
const actors = new Set(list.map((intent) => intent.actor).filter(Boolean));
|
|
61
|
+
if (list.length > 1 && actors.size > 1) {
|
|
62
|
+
warnings.push({
|
|
63
|
+
code: "conflicting-open-intents",
|
|
64
|
+
target,
|
|
65
|
+
message: `Multiple agents have open environment intents for ${target}. Resolve or coordinate before changing it.`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
37
69
|
return warnings;
|
|
38
70
|
}
|
|
71
|
+
|
|
72
|
+
function inferTarget(action = "") {
|
|
73
|
+
const normalized = String(action).toLowerCase();
|
|
74
|
+
for (const target of ["node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
|
|
75
|
+
if (normalized.includes(target)) return target;
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
}
|
package/src/inventory.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { commandOutput } from "./shell.js";
|
|
2
|
+
|
|
3
|
+
const MAX_ITEMS = 40;
|
|
4
|
+
|
|
5
|
+
export async function scanGlobalInventory(options = {}) {
|
|
6
|
+
if (!options.deep) {
|
|
7
|
+
return {
|
|
8
|
+
mode: "basic",
|
|
9
|
+
enabled: false,
|
|
10
|
+
note: "Run `aienvmp sync --deep` to collect read-only global tool inventory."
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
mode: "deep",
|
|
16
|
+
enabled: true,
|
|
17
|
+
note: "Read-only global tool inventory. Use for AI awareness, not enforcement.",
|
|
18
|
+
tools: compact({
|
|
19
|
+
npmGlobal: await scanNpmGlobal(),
|
|
20
|
+
pipx: await scanPipx(),
|
|
21
|
+
uvTools: await scanUvTools(),
|
|
22
|
+
brew: await scanBrew()
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function scanNpmGlobal() {
|
|
28
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
29
|
+
const raw = await commandOutput(command, ["list", "-g", "--depth=0", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
|
|
30
|
+
return parseNpmGlobal(raw);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function scanPipx() {
|
|
34
|
+
const raw = await commandOutput("pipx", ["list", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
|
|
35
|
+
return parsePipx(raw);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function scanUvTools() {
|
|
39
|
+
const raw = await commandOutput("uv", ["tool", "list"], { timeout: 5000 });
|
|
40
|
+
return parseNameVersionLines(raw);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function scanBrew() {
|
|
44
|
+
if (process.platform === "win32") return [];
|
|
45
|
+
const raw = await commandOutput("brew", ["list", "--versions"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
|
|
46
|
+
return parseNameVersionLines(raw);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseNpmGlobal(raw) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return Object.entries(parsed.dependencies || {})
|
|
53
|
+
.slice(0, MAX_ITEMS)
|
|
54
|
+
.map(([name, value]) => ({ name, version: value.version || "unknown" }));
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parsePipx(raw) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(raw);
|
|
63
|
+
return Object.entries(parsed.venvs || {})
|
|
64
|
+
.slice(0, MAX_ITEMS)
|
|
65
|
+
.map(([name, value]) => ({
|
|
66
|
+
name,
|
|
67
|
+
version: value.metadata?.main_package?.package_version || "unknown"
|
|
68
|
+
}));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseNameVersionLines(raw) {
|
|
75
|
+
return String(raw || "")
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.slice(0, MAX_ITEMS)
|
|
80
|
+
.map((line) => {
|
|
81
|
+
const [name, ...rest] = line.split(/\s+/);
|
|
82
|
+
return { name, version: rest.join(" ") || "unknown" };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compact(obj) {
|
|
87
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => Array.isArray(value) && value.length));
|
|
88
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -3,8 +3,10 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { commandOutput, commandVersion } from "./shell.js";
|
|
5
5
|
import { exists } from "./fsutil.js";
|
|
6
|
+
import { observedTrust } from "./trust.js";
|
|
7
|
+
import { scanGlobalInventory } from "./inventory.js";
|
|
6
8
|
|
|
7
|
-
export async function buildManifest(dir) {
|
|
9
|
+
export async function buildManifest(dir, options = {}) {
|
|
8
10
|
const now = new Date().toISOString();
|
|
9
11
|
const manifest = {
|
|
10
12
|
schemaName: "aienvmp.runtime-sbom",
|
|
@@ -12,8 +14,9 @@ export async function buildManifest(dir) {
|
|
|
12
14
|
generatedAt: now,
|
|
13
15
|
generatedBy: {
|
|
14
16
|
name: "aienvmp",
|
|
15
|
-
command: "aienvmp sync"
|
|
17
|
+
command: options.deep ? "aienvmp sync --deep" : "aienvmp sync"
|
|
16
18
|
},
|
|
19
|
+
trust: observedTrust(new Date(now)),
|
|
17
20
|
workspace: {
|
|
18
21
|
path: dir,
|
|
19
22
|
name: path.basename(dir)
|
|
@@ -23,13 +26,20 @@ export async function buildManifest(dir) {
|
|
|
23
26
|
packageManagers: await scanPackageManagers(),
|
|
24
27
|
containers: await scanContainers(),
|
|
25
28
|
projectHints: await scanProjectHints(dir),
|
|
29
|
+
inventory: await scanGlobalInventory({ deep: options.deep }),
|
|
26
30
|
agentFiles: await scanAgentFiles(dir),
|
|
27
31
|
agentProtocol: {
|
|
28
32
|
sourceOfTruth: "AIENV.md",
|
|
29
33
|
preflightCommand: "aienvmp context",
|
|
34
|
+
handoffCommand: "aienvmp handoff",
|
|
30
35
|
intentCommand: "aienvmp intent --actor <agent:id> --action <planned-change>",
|
|
31
36
|
recordCommand: "aienvmp record --actor <agent:id> --summary <what-changed>",
|
|
32
37
|
afterEnvironmentChange: ["aienvmp sync"],
|
|
38
|
+
trustModel: {
|
|
39
|
+
agentWritable: ["observed", "planned", "changed", "review", "stale"],
|
|
40
|
+
verifiedRequires: "human-or-ci",
|
|
41
|
+
rule: "AI agents may report observations and plans, but must not mark environment facts as verified."
|
|
42
|
+
},
|
|
33
43
|
globalRuntimeChangeRequiresUserApproval: true,
|
|
34
44
|
globalInstallPolicy: "ask-first",
|
|
35
45
|
projectLocalChanges: "allowed-when-task-requires"
|
|
@@ -61,6 +71,13 @@ export async function buildManifest(dir) {
|
|
|
61
71
|
docker: "docker --version",
|
|
62
72
|
compose: "docker compose version or docker-compose --version"
|
|
63
73
|
},
|
|
74
|
+
globalInventory: {
|
|
75
|
+
mode: "basic by default; deep only when requested",
|
|
76
|
+
npmGlobal: "npm list -g --depth=0 --json",
|
|
77
|
+
pipx: "pipx list --json",
|
|
78
|
+
uvTools: "uv tool list",
|
|
79
|
+
brew: "brew list --versions"
|
|
80
|
+
},
|
|
64
81
|
projectHints: [
|
|
65
82
|
".nvmrc",
|
|
66
83
|
".python-version",
|
package/src/render.js
CHANGED
|
@@ -19,12 +19,17 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
19
19
|
lines.push(...policyLines(policy));
|
|
20
20
|
lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
|
|
21
21
|
lines.push("- Project-local dependency installs: allowed when required by the user task.", "");
|
|
22
|
+
lines.push("## Trust State", "");
|
|
23
|
+
lines.push(`- State: ${manifest.trust?.state || "observed"}`);
|
|
24
|
+
lines.push("- Rule: AI agents may observe, plan, and record changes, but verified requires human or CI review.", "");
|
|
22
25
|
lines.push("## AI Preflight Summary", "");
|
|
23
26
|
lines.push(...contextLines(manifest, warnings, intents), "");
|
|
24
27
|
lines.push("## Runtime Map", "");
|
|
25
28
|
pushMap(lines, "Runtimes", manifest.runtimes);
|
|
26
29
|
pushMap(lines, "Package Managers", manifest.packageManagers);
|
|
27
30
|
pushMap(lines, "Containers", manifest.containers);
|
|
31
|
+
lines.push("## Global Tool Inventory", "");
|
|
32
|
+
lines.push(...inventoryLines(manifest.inventory), "");
|
|
28
33
|
lines.push("## Project Requirements And Hints", "");
|
|
29
34
|
pushMap(lines, "Detected", manifest.projectHints);
|
|
30
35
|
lines.push("## Drift And Warnings", "");
|
|
@@ -83,10 +88,12 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
83
88
|
"",
|
|
84
89
|
`Status: ${status}`,
|
|
85
90
|
`Next: ${next}`,
|
|
91
|
+
`Trust: ${manifest.trust?.state || "observed"} (verified requires human or CI)`,
|
|
86
92
|
`Workspace: ${manifest.workspace.path}`,
|
|
87
93
|
`Node: ${manifest.runtimes.node || "not detected"}`,
|
|
88
94
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
89
95
|
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
96
|
+
`Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
90
97
|
`Policy Node: ${policy.node || "not set"}`,
|
|
91
98
|
`Policy Python: ${policy.python || "not set"}`,
|
|
92
99
|
`Policy Package Manager: ${policy.packageManager || "not set"}`,
|
|
@@ -116,12 +123,15 @@ export function renderHandoff(handoff) {
|
|
|
116
123
|
"# AI Handoff",
|
|
117
124
|
"",
|
|
118
125
|
`Status: ${handoff.status}`,
|
|
126
|
+
`Trust: ${handoff.trust?.state || "observed"} (not AI-verified)`,
|
|
127
|
+
`Schema: ${handoff.schemaVersion}`,
|
|
119
128
|
`Workspace: ${handoff.workspace?.path || "unknown"}`,
|
|
120
129
|
"",
|
|
121
130
|
"Safe runtime:",
|
|
122
131
|
`- Node: ${handoff.safeRuntime.node}`,
|
|
123
132
|
`- Python: ${handoff.safeRuntime.python}`,
|
|
124
133
|
`- Docker: ${handoff.safeRuntime.docker}`,
|
|
134
|
+
`- Inventory: ${handoff.inventory?.mode || "basic"}${handoff.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
125
135
|
"",
|
|
126
136
|
"Open intents:",
|
|
127
137
|
...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
|
|
@@ -197,6 +207,9 @@ const {manifest,timeline,warnings,intents,policy}=JSON.parse(document.getElement
|
|
|
197
207
|
function esc(s){return String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"')}
|
|
198
208
|
const entries=o=>Object.entries(o||{});
|
|
199
209
|
const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(String(v))}</code></td></tr>\`).join('')||'<tr><td colspan="2">None detected</td></tr>';
|
|
210
|
+
const inventoryGroups=manifest.inventory?.tools||{};
|
|
211
|
+
const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
|
|
212
|
+
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>';
|
|
200
213
|
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
201
214
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
202
215
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
@@ -208,9 +221,11 @@ const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<di
|
|
|
208
221
|
const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
|
|
209
222
|
const reviewRequired=warnings.length>0||intents.length>0;
|
|
210
223
|
const recentChanges=timeline.slice(-8).length;
|
|
224
|
+
const trustState=manifest.trust?.state||'observed';
|
|
211
225
|
const nextAction=reviewRequired?'Review before environment changes':'Proceed with project-local work';
|
|
212
226
|
const auditItem=(key,value,hint,klass='')=>\`<div class="audit-item \${klass}"><div class="audit-k">\${key}</div><div class="audit-v">\${value}</div><div class="audit-hint">\${hint}</div></div>\`;
|
|
213
|
-
const
|
|
227
|
+
const driftLabel=warnings.length?'detected':'none';
|
|
228
|
+
const handoffHtml=\`<table><tr><th>Status</th><td>\${reviewRequired?'review-required':'clear'}</td></tr><tr><th>Trust</th><td><code>\${esc(trustState)}</code></td></tr><tr><th>Node</th><td><code>\${esc(manifest.runtimes.node||'not detected')}</code></td></tr><tr><th>Python</th><td><code>\${esc(manifest.runtimes.python||manifest.runtimes.python3||'not detected')}</code></td></tr><tr><th>Docker</th><td>\${manifest.containers?.docker?'available':'not detected'}</td></tr><tr><th>Next</th><td>\${reviewRequired?'Review warnings and open intents':'Continue project-local work'}</td></tr></table>\`;
|
|
214
229
|
document.getElementById('app').innerHTML=\`
|
|
215
230
|
<header>
|
|
216
231
|
<div>
|
|
@@ -222,9 +237,9 @@ document.getElementById('app').innerHTML=\`
|
|
|
222
237
|
</header>
|
|
223
238
|
<section class="audit" aria-label="Audit summary">
|
|
224
239
|
\${auditItem('AI decision',reviewRequired?'review required':'can proceed',nextAction,reviewRequired?'review':'primary')}
|
|
225
|
-
\${auditItem('
|
|
226
|
-
\${auditItem('
|
|
227
|
-
\${auditItem('
|
|
240
|
+
\${auditItem('Runtime drift',driftLabel,warnings.length?'Policy, runtime, or coordination warning detected':'No drift warnings detected',warnings.length?'review':'')}
|
|
241
|
+
\${auditItem('Open env changes',String(intents.length),intents.length?'Resolve or coordinate before changes':'No pending env changes')}
|
|
242
|
+
\${auditItem('Trust',trustState,trustState==='verified'?'Human or CI verified':'Machine observed; not AI-verified')}
|
|
228
243
|
</section>
|
|
229
244
|
<section class="metrics">
|
|
230
245
|
<div class="metric"><div class="num">\${entries(manifest.runtimes).length}</div><div class="label">runtimes</div></div>
|
|
@@ -238,6 +253,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
238
253
|
\${card('Package Managers',\`<span class="pill">\${entries(manifest.packageManagers).length} found</span>\`,\`<table>\${rows(manifest.packageManagers)}</table>\`)}
|
|
239
254
|
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
240
255
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
256
|
+
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
241
257
|
</div>
|
|
242
258
|
<aside>
|
|
243
259
|
\${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
|
|
@@ -296,6 +312,17 @@ function contextLines(manifest, warnings, intents) {
|
|
|
296
312
|
];
|
|
297
313
|
}
|
|
298
314
|
|
|
315
|
+
function inventoryLines(inventory = {}) {
|
|
316
|
+
if (!inventory.enabled) return ["- Mode: basic", "- Deep global inventory is disabled. Run `aienvmp sync --deep` when needed."];
|
|
317
|
+
const groups = Object.entries(inventory.tools || {});
|
|
318
|
+
if (!groups.length) return ["- Mode: deep", "- No global tools detected by optional scanners."];
|
|
319
|
+
const lines = ["- Mode: deep"];
|
|
320
|
+
for (const [name, items] of groups) {
|
|
321
|
+
lines.push(`- ${name}: ${items.length} tools`);
|
|
322
|
+
}
|
|
323
|
+
return lines;
|
|
324
|
+
}
|
|
325
|
+
|
|
299
326
|
function policyLines(policy) {
|
|
300
327
|
const lines = [];
|
|
301
328
|
if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
|
package/src/shell.js
CHANGED
|
@@ -15,10 +15,11 @@ export async function commandVersion(command, args = ["--version"]) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export async function commandOutput(command, args = []) {
|
|
18
|
+
export async function commandOutput(command, args = [], options = {}) {
|
|
19
19
|
try {
|
|
20
20
|
const { stdout } = await execFileAsync(command, args, {
|
|
21
|
-
timeout: 2500,
|
|
21
|
+
timeout: options.timeout || 2500,
|
|
22
|
+
maxBuffer: options.maxBuffer || 1024 * 1024,
|
|
22
23
|
windowsHide: true
|
|
23
24
|
});
|
|
24
25
|
return stdout.trim();
|
package/src/trust.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const TRUST_STATES = {
|
|
2
|
+
OBSERVED: "observed",
|
|
3
|
+
PLANNED: "planned",
|
|
4
|
+
CHANGED: "changed",
|
|
5
|
+
REVIEW: "review",
|
|
6
|
+
VERIFIED: "verified",
|
|
7
|
+
STALE: "stale"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function observedTrust(now = new Date()) {
|
|
11
|
+
return {
|
|
12
|
+
state: TRUST_STATES.OBSERVED,
|
|
13
|
+
at: now.toISOString(),
|
|
14
|
+
by: "aienvmp scan",
|
|
15
|
+
verified: false,
|
|
16
|
+
note: "Machine-observed only. AI agents cannot mark environment facts as verified."
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function plannedTrust(now = new Date()) {
|
|
21
|
+
return {
|
|
22
|
+
state: TRUST_STATES.PLANNED,
|
|
23
|
+
at: now.toISOString(),
|
|
24
|
+
verified: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function changedTrust(now = new Date(), requiresReview = false) {
|
|
29
|
+
return {
|
|
30
|
+
state: requiresReview ? TRUST_STATES.REVIEW : TRUST_STATES.CHANGED,
|
|
31
|
+
at: now.toISOString(),
|
|
32
|
+
verified: false
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isStaleTimestamp(value, now = new Date(), maxAgeHours = 24) {
|
|
37
|
+
if (!value) return false;
|
|
38
|
+
const then = new Date(value).getTime();
|
|
39
|
+
if (!Number.isFinite(then)) return false;
|
|
40
|
+
return now.getTime() - then > maxAgeHours * 60 * 60 * 1000;
|
|
41
|
+
}
|