agent-security-lens 0.1.8 → 0.1.9

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/.mcp/server.json CHANGED
@@ -2,18 +2,18 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.professor2k8/agent-security-lens",
4
4
  "title": "Agent Security Lens (AgentSecurityLens)",
5
- "description": "Safe-install and trust-check MCP for agents to review MCPs, Skills and tools before installation.",
5
+ "description": "Safe-install and trust-check MCP for agents before installing MCPs, Skills or tools.",
6
6
  "status": "active",
7
7
  "repository": {
8
8
  "url": "https://github.com/professor2k8/agent-security-lens",
9
9
  "source": "github"
10
10
  },
11
- "version": "0.1.8",
11
+ "version": "0.1.9",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "agent-security-lens",
16
- "version": "0.1.8",
16
+ "version": "0.1.9",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },
package/AGENTS.md CHANGED
@@ -6,6 +6,12 @@ If you are an autonomous agent, coding agent, workflow agent or tool-installing
6
6
 
7
7
  ## Quick Decision
8
8
 
9
+ First check ASL health and intelligence coverage:
10
+
11
+ ```bash
12
+ npx -y agent-security-lens doctor
13
+ ```
14
+
9
15
  Run a pre-install decision check:
10
16
 
11
17
  ```bash
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9
4
+
5
+ - Add `agent-security-lens doctor` so agents can check cloud reachability, local fallback coverage and queued telemetry before first use.
6
+ - Fix cloud review normalization so valid ASL Cloud install decisions are accepted instead of falling back to local mode.
7
+ - Resolve common GitHub monorepo URLs and install commands, including `modelcontextprotocol/servers/.../filesystem`, to canonical known components.
8
+ - Canonicalize cloud component display names on the client so agent-facing output uses stable component names.
9
+ - Improve operations metrics by tracking explicit npm daily ranges instead of relying only on cached point windows.
10
+
3
11
  ## 0.1.8
4
12
 
5
13
  - Add copy-ready first-call examples for 10 common MCP install decisions.
package/README.md CHANGED
@@ -21,6 +21,12 @@ Machine-readable agent instructions are also available in [AGENTS.md](AGENTS.md)
21
21
 
22
22
  ## Agent Quick Install
23
23
 
24
+ Check whether ASL can reach online intelligence and what local fallback is available:
25
+
26
+ ```bash
27
+ npx -y agent-security-lens doctor
28
+ ```
29
+
24
30
  Try one pre-install decision first:
25
31
 
26
32
  ```bash
@@ -132,6 +138,7 @@ Never submit private source code, secrets, tokens, cookies, or file contents.
132
138
  ## MCP Tools
133
139
 
134
140
  - `get_install_policy`: returns the current Agent execution policy.
141
+ - `get_intelligence_status`: reports online or local intelligence status for MCP clients.
135
142
  - `review_before_install`: evaluates a proposed component and installation context.
136
143
  - `check_component`: retrieves known component intelligence.
137
144
  - `recommend_alternatives`: returns evidence-backed alternatives and mitigations.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema_version": "0.1.0",
3
3
  "package": "agent-security-lens",
4
- "version": "0.1.8",
5
- "generated_at": "2026-06-25T04:55:00.570Z",
4
+ "version": "0.1.9",
5
+ "generated_at": "2026-06-27T08:42:28.272Z",
6
6
  "source": "ASL verified public release exporter",
7
7
  "files": [
8
8
  {
@@ -47,8 +47,8 @@
47
47
  },
48
48
  {
49
49
  "path": ".mcp/server.json",
50
- "bytes": 1316,
51
- "sha256": "a9dee7dbc493a6d4ebf6ceadf67d569c41cd5c1e0b7b63addf715845ed1595d8"
50
+ "bytes": 1303,
51
+ "sha256": "0ffe3bff5b51969977aa55a867698b72cdf7d1f3bd1b14c267d7979d5da92cb5"
52
52
  },
53
53
  {
54
54
  "path": ".npmignore",
@@ -57,13 +57,13 @@
57
57
  },
58
58
  {
59
59
  "path": "AGENTS.md",
60
- "bytes": 1973,
61
- "sha256": "320aba3d74cdec74b59ff4607abbe1fb920728daca44986aea115c377c563412"
60
+ "bytes": 2071,
61
+ "sha256": "2acf50a851a3ca8ec275f8c1bbdb198be44e519a9956a615136f8b4215fc51b8"
62
62
  },
63
63
  {
64
64
  "path": "CHANGELOG.md",
65
- "bytes": 3077,
66
- "sha256": "24dc14cfab212aff8e76624a9783533823fde9205234f6c4b507b96d87d6c2e6"
65
+ "bytes": 3723,
66
+ "sha256": "72baedc3dca9c034ed74d793c9ebb3df7109f595f31d6633651d6327fb7b801c"
67
67
  },
68
68
  {
69
69
  "path": "CODE_OF_CONDUCT.md",
@@ -92,8 +92,8 @@
92
92
  },
93
93
  {
94
94
  "path": "README.md",
95
- "bytes": 8074,
96
- "sha256": "4b7bd80bb553596693f5684341aeda4e61331e8f32c2f21fe0916edfefd521d3"
95
+ "bytes": 8298,
96
+ "sha256": "6a5a2f3afe1e5a606b953ca0fe224b0426466add17937962676c3cf62efb6931"
97
97
  },
98
98
  {
99
99
  "path": "SECURITY.md",
@@ -102,13 +102,13 @@
102
102
  },
103
103
  {
104
104
  "path": "apps/mcp-server/agent-security-lens-mcp.mjs",
105
- "bytes": 15475,
106
- "sha256": "b52fd8db2d26868d5403664420b631f08e7e1616b787b61644d79b647c1884e7"
105
+ "bytes": 15992,
106
+ "sha256": "d274f8a2973b66cfaa215e0c614017027836e6619c8ee842ba0be5399ce84f60"
107
107
  },
108
108
  {
109
109
  "path": "bin/agent-security-lens-review.mjs",
110
- "bytes": 7016,
111
- "sha256": "a11cfd8a70882c7d4ba4aa3af58f184db1acbfa4aa1a68e08f77ca4c232278f3"
110
+ "bytes": 7046,
111
+ "sha256": "49850a23241b275530b41a75068f1511b004d6dc8f55047e02ba9b616536c151"
112
112
  },
113
113
  {
114
114
  "path": "bin/agent-security-lens.mjs",
@@ -287,13 +287,13 @@
287
287
  },
288
288
  {
289
289
  "path": "llms.txt",
290
- "bytes": 2803,
291
- "sha256": "8e15f959d9f2a24155b606136d4c53d7029e97d948904f8874e393e58541ea78"
290
+ "bytes": 3059,
291
+ "sha256": "74052dfc82b3e3b36752a3d76039fa3e248f4afec0f3bb1bd1fe6505edbda5a0"
292
292
  },
293
293
  {
294
294
  "path": "package.json",
295
295
  "bytes": 2568,
296
- "sha256": "8eb4b7416328821604b823bdb9f3676e46b00dfdd12733d64fc65b8a1623d376"
296
+ "sha256": "5f8c05a09aa2e944c5fd68e57e21b2979f3111f6744c36761cc7329df9018bd4"
297
297
  },
298
298
  {
299
299
  "path": "profiles/generic-agent/profile.json",
@@ -412,8 +412,8 @@
412
412
  },
413
413
  {
414
414
  "path": "scripts/verify-mcp-server.mjs",
415
- "bytes": 9573,
416
- "sha256": "acc6e8b27c48a8fc3d1804f739f0755ed4104a408115c53fefc9338d3f91698b"
415
+ "bytes": 10549,
416
+ "sha256": "8409444a8c52c3cd9860ad70be9fd29e8d31e05eff4c5f3dbd88eeefa6364f1a"
417
417
  },
418
418
  {
419
419
  "path": "scripts/verify-registry.mjs",
@@ -422,8 +422,8 @@
422
422
  },
423
423
  {
424
424
  "path": "server.json",
425
- "bytes": 1316,
426
- "sha256": "a9dee7dbc493a6d4ebf6ceadf67d569c41cd5c1e0b7b63addf715845ed1595d8"
425
+ "bytes": 1303,
426
+ "sha256": "0ffe3bff5b51969977aa55a867698b72cdf7d1f3bd1b14c267d7979d5da92cb5"
427
427
  },
428
428
  {
429
429
  "path": "src/assessment/assess.mjs",
@@ -457,13 +457,18 @@
457
457
  },
458
458
  {
459
459
  "path": "src/intelligence/component-intelligence.mjs",
460
- "bytes": 14214,
461
- "sha256": "f8987fa64abd0a89288a6b2d79670b0fd2666ac8a7ccd13a9fa94e7e5d3627d3"
460
+ "bytes": 15288,
461
+ "sha256": "11c887108c13fd7f303b3667818875616b7b31516fe417bcb92334c9895171d2"
462
462
  },
463
463
  {
464
464
  "path": "src/intelligence/decision-engine.mjs",
465
- "bytes": 30122,
466
- "sha256": "b0a7b7cf27cdbbd41b2dc7e2805c8f43919adc13c53de5ceee7521f3574ff342"
465
+ "bytes": 32292,
466
+ "sha256": "97c75f4225175b67aa75e1e46a3c87a0594dd19e9de18790a1f5b05a49be8e01"
467
+ },
468
+ {
469
+ "path": "src/intelligence/doctor.mjs",
470
+ "bytes": 3564,
471
+ "sha256": "fb9223d83598838b38c48f1b5ea0b61788d1dbe7ef0336b25bb4b60262dc82d5"
467
472
  },
468
473
  {
469
474
  "path": "src/intelligence/finding-context.mjs",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { assess } from "../../src/assessment/assess.mjs";
4
4
  import { discoverTargets } from "../../src/assessment/discover-targets.mjs";
5
+ import { renderDoctorConsole, runDoctor } from "../../src/intelligence/doctor.mjs";
5
6
  import { apiEndpoints, cloudIntelligenceEnabled, queryCloudStatus } from "../../src/intelligence/cloud-client.mjs";
6
7
  import {
7
8
  getResearchStatus,
@@ -17,8 +18,20 @@ const serverInfo = {
17
18
  version: "0.1.0"
18
19
  };
19
20
 
21
+ if (process.argv[2] === "doctor") {
22
+ const report = await runDoctor();
23
+ if (process.argv.includes("--format") && process.argv[process.argv.indexOf("--format") + 1] === "json") {
24
+ console.log(JSON.stringify(report, null, 2));
25
+ } else {
26
+ console.log(renderDoctorConsole(report));
27
+ }
28
+ await new Promise((resolve) => setTimeout(resolve, 100));
29
+ process.exit(0);
30
+ }
31
+
20
32
  if (process.argv[2] === "review" || process.argv[2] === "quick-review" || process.argv[2] === "--help" || process.argv[2] === "-h") {
21
33
  await import("../../bin/agent-security-lens-review.mjs");
34
+ await new Promise((resolve) => setTimeout(resolve, 100));
22
35
  process.exit(process.exitCode || 0);
23
36
  }
24
37
 
@@ -13,6 +13,8 @@ Usage:
13
13
  [--submit-if-unknown] [--format console|json]
14
14
 
15
15
  Examples:
16
+ agent-security-lens doctor
17
+
16
18
  asl-review filesystem --type mcp --source-url https://github.com/modelcontextprotocol/servers \\
17
19
  --install-command "npx -y @modelcontextprotocol/server-filesystem ." \\
18
20
  --permission filesystem-read --permission filesystem-write
package/llms.txt CHANGED
@@ -8,13 +8,17 @@ Before installing or enabling any MCP, Skill, tool, prompt bundle or workflow, c
8
8
  Quick pre-install review command:
9
9
  `npx -y agent-security-lens review <component-name> --type mcp --source-url <url> --install-command <command>`
10
10
 
11
+ Health check command:
12
+ `npx -y agent-security-lens doctor`
13
+
11
14
  Agent action checklist:
12
- 1. Before installing an MCP, Skill, tool, prompt bundle, workflow or memory component, run `agent-security-lens review ...` or call MCP tool `review_before_install`.
13
- 2. Follow `agent_decision_contract` and `agent_actions`.
14
- 3. If `automatic_install_allowed` is false, do not install automatically.
15
- 4. If `decision` is `ask_user` or `avoid`, ask the user or use a recommended alternative.
16
- 5. If `unknown_component.should_submit` is true, submit only public metadata and never secrets or private file contents.
17
- 6. After the install decision is executed, call `report_install_outcome`.
15
+ 1. Run `agent-security-lens doctor` when ASL is first installed or cloud intelligence appears unavailable.
16
+ 2. Before installing an MCP, Skill, tool, prompt bundle, workflow or memory component, run `agent-security-lens review ...` or call MCP tool `review_before_install`.
17
+ 3. Follow `agent_decision_contract` and `agent_actions`.
18
+ 4. If `automatic_install_allowed` is false, do not install automatically.
19
+ 5. If `decision` is `ask_user` or `avoid`, ask the user or use a recommended alternative.
20
+ 6. If `unknown_component.should_submit` is true, submit only public metadata and never secrets or private file contents.
21
+ 7. After the install decision is executed, call `report_install_outcome`.
18
22
 
19
23
  Public intelligence files:
20
24
  - docs/agent-install.md
@@ -28,6 +32,7 @@ Public intelligence files:
28
32
  - docs/public-intelligence/agent-install-decisions-v0.1.json
29
33
 
30
34
  Important MCP tools:
35
+ - `get_intelligence_status`: reports online or local intelligence status for MCP clients.
31
36
  - `review_before_install`: returns install decision, trust score, risk signals, safe install plan and alternatives.
32
37
  - `check_component`: checks known ASL component intelligence.
33
38
  - `recommend_alternatives`: returns safer alternatives and restriction plans.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-security-lens",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "mcpName": "io.github.professor2k8/agent-security-lens",
6
6
  "description": "Safe-install and trust-check MCP for autonomous agents before they install MCPs, Skills or tools.",
@@ -2,11 +2,14 @@
2
2
 
3
3
  import { spawn } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
+ import { mkdtemp, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
5
7
  import { join } from "node:path";
6
8
 
9
+ const localQueueDir = await mkdtemp(join(tmpdir(), "asl-mcp-smoke-"));
7
10
  const child = spawn(process.execPath, ["./apps/mcp-server/agent-security-lens-mcp.mjs"], {
8
11
  cwd: process.cwd(),
9
- env: { ...process.env, ASL_MODE: "local" },
12
+ env: { ...process.env, ASL_MODE: "local", ASL_LOCAL_QUEUE_DIR: localQueueDir },
10
13
  stdio: ["pipe", "pipe", "pipe"]
11
14
  });
12
15
 
@@ -107,6 +110,18 @@ send({
107
110
  }
108
111
  }
109
112
  });
113
+ send({
114
+ jsonrpc: "2.0",
115
+ id: 9,
116
+ method: "tools/call",
117
+ params: {
118
+ name: "review_before_install",
119
+ arguments: {
120
+ component_name: "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
121
+ component_type: "mcp"
122
+ }
123
+ }
124
+ });
110
125
 
111
126
  const responseDeadline = Date.now() + 5000;
112
127
  while (Date.now() < responseDeadline) {
@@ -122,7 +137,7 @@ while (Date.now() < responseDeadline) {
122
137
  }
123
138
  })
124
139
  );
125
- if ([1, 2, 3, 4, 5, 6, 7, 8].every((id) => responseIds.has(id))) break;
140
+ if ([1, 2, 3, 4, 5, 6, 7, 8, 9].every((id) => responseIds.has(id))) break;
126
141
  await new Promise((resolve) => setTimeout(resolve, 50));
127
142
  }
128
143
  child.kill();
@@ -193,6 +208,18 @@ if (!reviewJson.agent_actions?.some((action) => action.id === "report-install-ou
193
208
  process.exit(1);
194
209
  }
195
210
 
211
+ const urlReview = lines.find((line) => line.id === 9);
212
+ const urlReviewJson = JSON.parse(urlReview?.result?.content?.[0]?.text || "{}");
213
+ if (
214
+ urlReviewJson.component?.id !== "mcp-filesystem" ||
215
+ urlReviewJson.component?.name !== "filesystem" ||
216
+ urlReviewJson.component?.known !== true
217
+ ) {
218
+ console.error("MCP smoke failed: GitHub monorepo URL did not resolve to canonical filesystem component");
219
+ console.error(output || errorOutput);
220
+ process.exit(1);
221
+ }
222
+
196
223
  const policy = lines.find((line) => line.id === 4);
197
224
  const policyText = policy?.result?.content?.[0]?.text || "";
198
225
  if (!policyText.includes("review_before_install") || !policyText.includes("agent_decision_contract")) {
@@ -276,3 +303,4 @@ if (!feedbackText.includes('"source": "local_fallback"') && !feedbackText.includ
276
303
  }
277
304
 
278
305
  console.log("mcp server: tools/list and review_before_install checked");
306
+ await rm(localQueueDir, { recursive: true, force: true });
package/server.json CHANGED
@@ -2,18 +2,18 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.professor2k8/agent-security-lens",
4
4
  "title": "Agent Security Lens (AgentSecurityLens)",
5
- "description": "Safe-install and trust-check MCP for agents to review MCPs, Skills and tools before installation.",
5
+ "description": "Safe-install and trust-check MCP for agents before installing MCPs, Skills or tools.",
6
6
  "status": "active",
7
7
  "repository": {
8
8
  "url": "https://github.com/professor2k8/agent-security-lens",
9
9
  "source": "github"
10
10
  },
11
- "version": "0.1.8",
11
+ "version": "0.1.9",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "agent-security-lens",
16
- "version": "0.1.8",
16
+ "version": "0.1.9",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },
@@ -55,7 +55,7 @@ function withResolution(review, resolution) {
55
55
  }
56
56
 
57
57
  function normalizeCloudReview(cloudData, input, cloudResult) {
58
- const review = cloudData?.result || cloudData?.decision || cloudData;
58
+ const review = cloudData?.result || (typeof cloudData?.decision === "object" ? cloudData.decision : null) || cloudData;
59
59
  if (!review || typeof review !== "object") return null;
60
60
  if (!review.decision || !review.trust_score || !Array.isArray(review.risk_signals)) return null;
61
61
  return withResolution(
@@ -107,6 +107,31 @@ function normalizeCloudReview(cloudData, input, cloudResult) {
107
107
  );
108
108
  }
109
109
 
110
+ async function canonicalizeCloudReview(review, input = {}) {
111
+ try {
112
+ const database = await loadComponentDatabase();
113
+ const localKnown =
114
+ database.components.find((component) => component.id && component.id === review.component?.id) ||
115
+ findKnownComponentRecord(input, database.components);
116
+ const currentName = review.component?.name || "";
117
+ const genericOrUrlName = /^https?:\/\//i.test(currentName) || /^(planned-install|install|component|unknown|tool|mcp|skill)$/i.test(currentName);
118
+ if (localKnown?.name && genericOrUrlName) {
119
+ return {
120
+ ...review,
121
+ component: {
122
+ ...review.component,
123
+ name: localKnown.name,
124
+ source_url: review.component?.source_url || localKnown.source_url || null,
125
+ full_name: review.component?.full_name || localKnown.aliases?.[1] || null
126
+ }
127
+ };
128
+ }
129
+ } catch {
130
+ // Cloud decisions remain usable even if local canonicalization is unavailable.
131
+ }
132
+ return review;
133
+ }
134
+
110
135
  async function buildLocalReview(input = {}, fallbackReason = null) {
111
136
  const database = await loadComponentDatabase();
112
137
  const candidates = await loadCandidateCatalog();
@@ -128,7 +153,7 @@ export async function reviewBeforeInstall(input = {}) {
128
153
  const cloudResult = await queryCloudReview(input);
129
154
  if (cloudResult.ok) {
130
155
  const normalized = normalizeCloudReview(cloudResult.data, input, cloudResult);
131
- if (normalized) return normalized;
156
+ if (normalized) return canonicalizeCloudReview(normalized, input);
132
157
  }
133
158
  return buildLocalReview(input, cloudResult.reason || "cloud_result_unusable");
134
159
  }
@@ -14,15 +14,69 @@ export function unique(items) {
14
14
  return [...new Set(items.filter(Boolean))];
15
15
  }
16
16
 
17
- function aliasMatchesInput(alias, input = {}) {
18
- const normalizedAlias = normalizeText(alias).trim();
19
- if (!normalizedAlias) return false;
20
- const exactFields = [
17
+ function normalizedUrlParts(value) {
18
+ const text = String(value || "").trim();
19
+ if (!/^https?:\/\//i.test(text)) return null;
20
+ try {
21
+ const url = new URL(text);
22
+ const pathParts = url.pathname.split("/").filter(Boolean);
23
+ return {
24
+ host: url.hostname.toLowerCase(),
25
+ pathParts,
26
+ owner: pathParts[0] || null,
27
+ repo: pathParts[1] || null,
28
+ tail: pathParts.at(-1) || null
29
+ };
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function extractPackageTokens(value) {
36
+ const text = String(value || "");
37
+ const tokens = [];
38
+ const scoped = text.matchAll(/@[a-z0-9._-]+\/[a-z0-9._-]+/gi);
39
+ for (const match of scoped) tokens.push(match[0]);
40
+ const npxLike = text.matchAll(/\b(?:npx|uvx|pnpm\s+dlx|yarn\s+dlx)\s+(?:-y\s+)?([a-z0-9._-]+\/[a-z0-9._-]+|[a-z0-9._-]+)/gi);
41
+ for (const match of npxLike) tokens.push(match[1]);
42
+ return tokens;
43
+ }
44
+
45
+ function derivedInputTerms(input = {}) {
46
+ const terms = [
21
47
  input.component_name,
22
48
  input.name,
23
49
  input.package_name,
24
- input.registry
25
- ].map((item) => normalizeText(item).trim());
50
+ input.registry,
51
+ ...extractPackageTokens(input.component_name),
52
+ ...extractPackageTokens(input.install_command)
53
+ ];
54
+
55
+ for (const value of [input.component_name, input.source_url]) {
56
+ const parsed = normalizedUrlParts(value);
57
+ if (!parsed) continue;
58
+ const { host, owner, repo, tail, pathParts } = parsed;
59
+ if (owner && repo) {
60
+ terms.push(`${owner}/${repo}`, `https://${host}/${owner}/${repo}`);
61
+ }
62
+ if (tail) {
63
+ terms.push(tail);
64
+ if (repo === "servers" && owner === "modelcontextprotocol") {
65
+ terms.push(`server-${tail}`, `@modelcontextprotocol/server-${tail}`, `modelcontextprotocol/server-${tail}`);
66
+ }
67
+ if (repo === "skills" && pathParts.includes("skills")) {
68
+ terms.push(tail);
69
+ }
70
+ }
71
+ }
72
+
73
+ return unique(terms.map((item) => normalizeText(item).trim()).filter(Boolean));
74
+ }
75
+
76
+ function aliasMatchesInput(alias, input = {}) {
77
+ const normalizedAlias = normalizeText(alias).trim();
78
+ if (!normalizedAlias) return false;
79
+ const exactFields = derivedInputTerms(input);
26
80
  if (exactFields.includes(normalizedAlias)) return true;
27
81
  if (normalizedAlias.length < 5) return false;
28
82
 
@@ -43,6 +97,14 @@ function aliasMatchesInput(alias, input = {}) {
43
97
  return searchableText.includes(normalizedAlias);
44
98
  }
45
99
 
100
+ function canonicalComponentName(input = {}, known = null) {
101
+ const inputName = input.component_name || input.name || null;
102
+ const looksLikeUrl = /^https?:\/\//i.test(String(inputName || ""));
103
+ const genericInputName = /^(planned-install|install|component|unknown|tool|mcp|skill)$/i.test(String(inputName || ""));
104
+ if (known?.name && (!inputName || looksLikeUrl || genericInputName)) return known.name;
105
+ return inputName || known?.name || null;
106
+ }
107
+
46
108
  const EVALUATION_MODEL_VERSION = "asl-safety-standard@0.2.0";
47
109
 
48
110
  const RISK_SIGNAL_WEIGHTS = {
@@ -702,7 +764,7 @@ export function buildInstallDecision({
702
764
  cataloged: Boolean(candidate),
703
765
  intelligence_state: intelligenceState,
704
766
  id: known?.id || null,
705
- name: input.component_name || input.name || known?.name || null,
767
+ name: canonicalComponentName(input, known),
706
768
  type: input.component_type || input.type || known?.type || "unknown",
707
769
  source_url: input.source_url || known?.source_url || null,
708
770
  full_name: known?.aliases?.[1] || null,
@@ -0,0 +1,97 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { apiEndpoints, cloudIntelligenceEnabled, queryCloudStatus } from "./cloud-client.mjs";
4
+ import { loadCandidateCatalog, loadComponentDatabase } from "./component-intelligence.mjs";
5
+
6
+ const LOCAL_QUEUE_ROOT = process.env.ASL_LOCAL_QUEUE_DIR || join(process.cwd(), ".agentsecuritylens");
7
+
8
+ async function countFiles(dir) {
9
+ try {
10
+ const entries = await readdir(dir, { withFileTypes: true });
11
+ return entries.filter((entry) => entry.isFile()).length;
12
+ } catch {
13
+ return 0;
14
+ }
15
+ }
16
+
17
+ function countBy(items, field) {
18
+ return items.reduce((acc, item) => {
19
+ const value = item[field] || "unknown";
20
+ acc[value] = (acc[value] || 0) + 1;
21
+ return acc;
22
+ }, {});
23
+ }
24
+
25
+ function cloudInstruction(cloud, onlineMode) {
26
+ if (!onlineMode) return "ASL is in local mode. Online intelligence and telemetry are disabled.";
27
+ if (cloud?.ok || cloud?.reachable) return "Cloud intelligence is reachable. Use review_before_install before installing components.";
28
+ return "Cloud intelligence is not reachable from this environment. Continue with local fallback and retry later.";
29
+ }
30
+
31
+ export async function runDoctor() {
32
+ const onlineMode = cloudIntelligenceEnabled();
33
+ const [database, candidates, cloud] = await Promise.all([
34
+ loadComponentDatabase(),
35
+ loadCandidateCatalog(),
36
+ onlineMode ? queryCloudStatus() : Promise.resolve(null)
37
+ ]);
38
+
39
+ const components = Array.isArray(database.components) ? database.components : [];
40
+ const queues = {
41
+ unknown_components: await countFiles(join(LOCAL_QUEUE_ROOT, "unknown-components")),
42
+ usage_events: await countFiles(join(LOCAL_QUEUE_ROOT, "usage-events")),
43
+ decision_feedback: await countFiles(join(LOCAL_QUEUE_ROOT, "decision-feedback"))
44
+ };
45
+
46
+ return {
47
+ schema_version: "0.1.0",
48
+ service: "AgentSecurityLens",
49
+ result_type: "doctor",
50
+ generated_at: new Date().toISOString(),
51
+ mode: onlineMode ? "online" : "local",
52
+ api_endpoints: apiEndpoints(),
53
+ cloud: cloud
54
+ ? {
55
+ reachable: cloud.ok,
56
+ api_url: cloud.api_url,
57
+ attempted_api_urls: cloud.attempted_api_urls || [],
58
+ reason: cloud.reason || null,
59
+ error: cloud.error || null
60
+ }
61
+ : null,
62
+ local_intelligence: {
63
+ components: components.length,
64
+ candidates: candidates.length,
65
+ by_state: countBy(components, "intelligence_state"),
66
+ by_type: countBy(components, "type")
67
+ },
68
+ local_queues: queues,
69
+ health: {
70
+ cloud_reachable: Boolean(cloud?.ok),
71
+ has_local_fallback: components.length > 0,
72
+ has_pending_local_telemetry: Object.values(queues).some((count) => count > 0)
73
+ },
74
+ agent_instruction: cloudInstruction(cloud, onlineMode)
75
+ };
76
+ }
77
+
78
+ export function renderDoctorConsole(report) {
79
+ const cloud = report.cloud;
80
+ const lines = [
81
+ "AgentSecurityLens doctor",
82
+ "",
83
+ `Mode: ${report.mode}`,
84
+ `Cloud reachable: ${cloud?.reachable ? "yes" : "no"}`,
85
+ `API endpoints: ${report.api_endpoints.join(", ") || "none"}`,
86
+ `Local components: ${report.local_intelligence.components}`,
87
+ `Local candidates: ${report.local_intelligence.candidates}`,
88
+ `Local queues: unknown=${report.local_queues.unknown_components}, usage=${report.local_queues.usage_events}, feedback=${report.local_queues.decision_feedback}`
89
+ ];
90
+
91
+ if (cloud && !cloud.reachable) {
92
+ lines.push(`Cloud issue: ${cloud.reason || "unknown"}${cloud.error ? ` (${cloud.error})` : ""}`);
93
+ }
94
+
95
+ lines.push("", report.agent_instruction);
96
+ return lines.join("\n");
97
+ }