agent-security-lens 0.1.7 → 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 +3 -3
- package/AGENTS.md +6 -0
- package/CHANGELOG.md +13 -0
- package/README.md +12 -0
- package/RELEASE-MANIFEST.json +35 -25
- package/apps/mcp-server/agent-security-lens-mcp.mjs +13 -0
- package/bin/agent-security-lens-review.mjs +2 -0
- package/docs/public-intelligence/mcp-first-call-examples-v0.1.md +169 -0
- package/llms.txt +12 -6
- package/package.json +1 -1
- package/scripts/verify-mcp-server.mjs +30 -2
- package/server.json +3 -3
- package/src/intelligence/component-intelligence.mjs +27 -2
- package/src/intelligence/decision-engine.mjs +69 -7
- package/src/intelligence/doctor.mjs +97 -0
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
|
|
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.
|
|
11
|
+
"version": "0.1.9",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "agent-security-lens",
|
|
16
|
-
"version": "0.1.
|
|
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,18 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.1.8
|
|
12
|
+
|
|
13
|
+
- Add copy-ready first-call examples for 10 common MCP install decisions.
|
|
14
|
+
- Link first-call examples from README and `llms.txt` so agents can discover concrete ASL review commands.
|
|
15
|
+
|
|
3
16
|
## 0.1.7
|
|
4
17
|
|
|
5
18
|
- Add root `AGENTS.md` so autonomous coding agents can discover ASL's pre-install policy directly from the repository.
|
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
|
|
@@ -36,6 +42,10 @@ The command returns an agent-readable decision, trust score, risk signals, safe
|
|
|
36
42
|
|
|
37
43
|
If your shell does not expose npm temporary binaries correctly, use `npm install -g agent-security-lens` once, then run the same command without `npx -y`.
|
|
38
44
|
|
|
45
|
+
More copy-ready calls:
|
|
46
|
+
|
|
47
|
+
- [MCP first-call examples](docs/public-intelligence/mcp-first-call-examples-v0.1.md)
|
|
48
|
+
|
|
39
49
|
Add ASL as a pre-install trust check MCP:
|
|
40
50
|
|
|
41
51
|
```json
|
|
@@ -73,6 +83,7 @@ The public package includes a reviewed baseline so agents can still make conserv
|
|
|
73
83
|
- [Top MCP Security Signals v0.1](docs/public-intelligence/top-mcp-security-signals-v0.1.md)
|
|
74
84
|
- [Top Skill Security Signals v0.1](docs/public-intelligence/top-skill-security-signals-v0.1.md)
|
|
75
85
|
- [Agent Framework Install Decisions v0.1](docs/public-intelligence/agent-framework-install-decisions-v0.1.md)
|
|
86
|
+
- [MCP First-Call Examples v0.1](docs/public-intelligence/mcp-first-call-examples-v0.1.md)
|
|
76
87
|
|
|
77
88
|
The online ASL intelligence API serves the current expanded catalog. The npm package intentionally does not ship the complete private intelligence database.
|
|
78
89
|
|
|
@@ -127,6 +138,7 @@ Never submit private source code, secrets, tokens, cookies, or file contents.
|
|
|
127
138
|
## MCP Tools
|
|
128
139
|
|
|
129
140
|
- `get_install_policy`: returns the current Agent execution policy.
|
|
141
|
+
- `get_intelligence_status`: reports online or local intelligence status for MCP clients.
|
|
130
142
|
- `review_before_install`: evaluates a proposed component and installation context.
|
|
131
143
|
- `check_component`: retrieves known component intelligence.
|
|
132
144
|
- `recommend_alternatives`: returns evidence-backed alternatives and mitigations.
|
package/RELEASE-MANIFEST.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "0.1.0",
|
|
3
3
|
"package": "agent-security-lens",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generated_at": "2026-06-
|
|
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":
|
|
51
|
-
"sha256": "
|
|
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":
|
|
61
|
-
"sha256": "
|
|
60
|
+
"bytes": 2071,
|
|
61
|
+
"sha256": "2acf50a851a3ca8ec275f8c1bbdb198be44e519a9956a615136f8b4215fc51b8"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"path": "CHANGELOG.md",
|
|
65
|
-
"bytes":
|
|
66
|
-
"sha256": "
|
|
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":
|
|
96
|
-
"sha256": "
|
|
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":
|
|
106
|
-
"sha256": "
|
|
105
|
+
"bytes": 15992,
|
|
106
|
+
"sha256": "d274f8a2973b66cfaa215e0c614017027836e6619c8ee842ba0be5399ce84f60"
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
"path": "bin/agent-security-lens-review.mjs",
|
|
110
|
-
"bytes":
|
|
111
|
-
"sha256": "
|
|
110
|
+
"bytes": 7046,
|
|
111
|
+
"sha256": "49850a23241b275530b41a75068f1511b004d6dc8f55047e02ba9b616536c151"
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
"path": "bin/agent-security-lens.mjs",
|
|
@@ -215,6 +215,11 @@
|
|
|
215
215
|
"bytes": 11404,
|
|
216
216
|
"sha256": "828fdcd367057a3b5583a80dc095b5e5365ef5e5f947ccf6ba95d964a1f50403"
|
|
217
217
|
},
|
|
218
|
+
{
|
|
219
|
+
"path": "docs/public-intelligence/mcp-first-call-examples-v0.1.md",
|
|
220
|
+
"bytes": 5457,
|
|
221
|
+
"sha256": "758bccdb7f962089549bb1d97be6479eaa497a12207643fb70d00e9d024cd71f"
|
|
222
|
+
},
|
|
218
223
|
{
|
|
219
224
|
"path": "docs/public-intelligence/top-mcp-security-signals-v0.1.md",
|
|
220
225
|
"bytes": 11894,
|
|
@@ -282,13 +287,13 @@
|
|
|
282
287
|
},
|
|
283
288
|
{
|
|
284
289
|
"path": "llms.txt",
|
|
285
|
-
"bytes":
|
|
286
|
-
"sha256": "
|
|
290
|
+
"bytes": 3059,
|
|
291
|
+
"sha256": "74052dfc82b3e3b36752a3d76039fa3e248f4afec0f3bb1bd1fe6505edbda5a0"
|
|
287
292
|
},
|
|
288
293
|
{
|
|
289
294
|
"path": "package.json",
|
|
290
295
|
"bytes": 2568,
|
|
291
|
-
"sha256": "
|
|
296
|
+
"sha256": "5f8c05a09aa2e944c5fd68e57e21b2979f3111f6744c36761cc7329df9018bd4"
|
|
292
297
|
},
|
|
293
298
|
{
|
|
294
299
|
"path": "profiles/generic-agent/profile.json",
|
|
@@ -407,8 +412,8 @@
|
|
|
407
412
|
},
|
|
408
413
|
{
|
|
409
414
|
"path": "scripts/verify-mcp-server.mjs",
|
|
410
|
-
"bytes":
|
|
411
|
-
"sha256": "
|
|
415
|
+
"bytes": 10549,
|
|
416
|
+
"sha256": "8409444a8c52c3cd9860ad70be9fd29e8d31e05eff4c5f3dbd88eeefa6364f1a"
|
|
412
417
|
},
|
|
413
418
|
{
|
|
414
419
|
"path": "scripts/verify-registry.mjs",
|
|
@@ -417,8 +422,8 @@
|
|
|
417
422
|
},
|
|
418
423
|
{
|
|
419
424
|
"path": "server.json",
|
|
420
|
-
"bytes":
|
|
421
|
-
"sha256": "
|
|
425
|
+
"bytes": 1303,
|
|
426
|
+
"sha256": "0ffe3bff5b51969977aa55a867698b72cdf7d1f3bd1b14c267d7979d5da92cb5"
|
|
422
427
|
},
|
|
423
428
|
{
|
|
424
429
|
"path": "src/assessment/assess.mjs",
|
|
@@ -452,13 +457,18 @@
|
|
|
452
457
|
},
|
|
453
458
|
{
|
|
454
459
|
"path": "src/intelligence/component-intelligence.mjs",
|
|
455
|
-
"bytes":
|
|
456
|
-
"sha256": "
|
|
460
|
+
"bytes": 15288,
|
|
461
|
+
"sha256": "11c887108c13fd7f303b3667818875616b7b31516fe417bcb92334c9895171d2"
|
|
457
462
|
},
|
|
458
463
|
{
|
|
459
464
|
"path": "src/intelligence/decision-engine.mjs",
|
|
460
|
-
"bytes":
|
|
461
|
-
"sha256": "
|
|
465
|
+
"bytes": 32292,
|
|
466
|
+
"sha256": "97c75f4225175b67aa75e1e46a3c87a0594dd19e9de18790a1f5b05a49be8e01"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"path": "src/intelligence/doctor.mjs",
|
|
470
|
+
"bytes": 3564,
|
|
471
|
+
"sha256": "fb9223d83598838b38c48f1b5ea0b61788d1dbe7ef0336b25bb4b60262dc82d5"
|
|
462
472
|
},
|
|
463
473
|
{
|
|
464
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
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# MCP First-Call Examples
|
|
2
|
+
|
|
3
|
+
These examples are for agents that need a fast pre-install decision before enabling an MCP server.
|
|
4
|
+
|
|
5
|
+
Run the command, read `agent_decision_contract`, and do not install automatically when `automatic_install_allowed` is `false`.
|
|
6
|
+
|
|
7
|
+
## Example Commands
|
|
8
|
+
|
|
9
|
+
### Filesystem
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y agent-security-lens review filesystem \
|
|
13
|
+
--type mcp \
|
|
14
|
+
--source-url https://github.com/modelcontextprotocol/servers \
|
|
15
|
+
--install-command "npx -y @modelcontextprotocol/server-filesystem ." \
|
|
16
|
+
--permission filesystem-read \
|
|
17
|
+
--permission filesystem-write
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Observed ASL summary: `ask_user`, trust score `8`, risk level `high`, coverage `curated_baseline`.
|
|
21
|
+
|
|
22
|
+
Primary signals: `filesystem-read`, `filesystem-write`, `subprocess-spawn`, `shell-execution`, `network-access`.
|
|
23
|
+
|
|
24
|
+
### GitHub
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx -y agent-security-lens review github \
|
|
28
|
+
--type mcp \
|
|
29
|
+
--source-url https://github.com/modelcontextprotocol/servers \
|
|
30
|
+
--install-command "npx -y @modelcontextprotocol/server-github" \
|
|
31
|
+
--permission network-access \
|
|
32
|
+
--permission credentials-access
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Observed ASL summary: `ask_user`, trust score `12`, risk level `medium`, coverage `curated_baseline`.
|
|
36
|
+
|
|
37
|
+
Primary signals: `network-access`, `credential-access`, `repository-write`, `shell-execution`.
|
|
38
|
+
|
|
39
|
+
### Postgres
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx -y agent-security-lens review postgres \
|
|
43
|
+
--type mcp \
|
|
44
|
+
--source-url https://github.com/modelcontextprotocol/servers \
|
|
45
|
+
--install-command "npx -y @modelcontextprotocol/server-postgres" \
|
|
46
|
+
--permission network-access \
|
|
47
|
+
--permission db-access
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Observed ASL summary: `ask_user`, trust score `10`, risk level `high`, coverage `curated_baseline`.
|
|
51
|
+
|
|
52
|
+
Primary signals: `database-access`, `credential-access`, `data-exposure-risk`, `shell-execution`, `network-access`.
|
|
53
|
+
|
|
54
|
+
### MongoDB
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx -y agent-security-lens review mongodb \
|
|
58
|
+
--type mcp \
|
|
59
|
+
--source-url https://github.com/mongodb-js/mongodb-mcp-server \
|
|
60
|
+
--install-command "npx -y mongodb-mcp-server" \
|
|
61
|
+
--permission network-access \
|
|
62
|
+
--permission db-access
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Observed ASL summary: `ask_user`, trust score `22`, risk level `medium`, coverage `monitored`.
|
|
66
|
+
|
|
67
|
+
Primary signals: `shell-execution`, `network-access`, `catalog-unreviewed`.
|
|
68
|
+
|
|
69
|
+
### Supabase
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx -y agent-security-lens review supabase \
|
|
73
|
+
--type mcp \
|
|
74
|
+
--source-url https://github.com/supabase-community/supabase-mcp \
|
|
75
|
+
--install-command "npx -y @supabase/mcp-server-supabase" \
|
|
76
|
+
--permission network-access \
|
|
77
|
+
--permission db-access \
|
|
78
|
+
--permission credentials-access
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Observed ASL summary: `ask_user`, trust score `22`, risk level `medium`, coverage `candidate`.
|
|
82
|
+
|
|
83
|
+
Primary signals: `shell-execution`, `network-access`, `catalog-unreviewed`.
|
|
84
|
+
|
|
85
|
+
### Grafana
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx -y agent-security-lens review grafana \
|
|
89
|
+
--type mcp \
|
|
90
|
+
--source-url https://github.com/grafana/mcp-grafana \
|
|
91
|
+
--install-command "npx -y mcp-grafana" \
|
|
92
|
+
--permission network-access \
|
|
93
|
+
--permission credentials-access
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Observed ASL summary: `ask_user`, trust score `17`, risk level `medium`, coverage `unknown`.
|
|
97
|
+
|
|
98
|
+
Primary signals: `shell-execution`, `network-access`, `unknown-source`.
|
|
99
|
+
|
|
100
|
+
### Slack
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx -y agent-security-lens review slack \
|
|
104
|
+
--type mcp \
|
|
105
|
+
--source-url https://github.com/modelcontextprotocol/servers \
|
|
106
|
+
--install-command "npx -y @modelcontextprotocol/server-slack" \
|
|
107
|
+
--permission network-access \
|
|
108
|
+
--permission credentials-access
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Observed ASL summary: `ask_user`, trust score `11`, risk level `medium`, coverage `curated_baseline`.
|
|
112
|
+
|
|
113
|
+
Primary signals: `network-access`, `credential-access`, `message-read`, `message-write`, `shell-execution`.
|
|
114
|
+
|
|
115
|
+
### Brave Search
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx -y agent-security-lens review brave-search \
|
|
119
|
+
--type mcp \
|
|
120
|
+
--source-url https://github.com/modelcontextprotocol/servers \
|
|
121
|
+
--install-command "npx -y @modelcontextprotocol/server-brave-search" \
|
|
122
|
+
--permission network-access \
|
|
123
|
+
--permission credentials-access
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Observed ASL summary: `ask_user`, trust score `12`, risk level `medium`, coverage `curated_baseline`.
|
|
127
|
+
|
|
128
|
+
Primary signals: `network-access`, `external-api`, `credential-access`, `shell-execution`.
|
|
129
|
+
|
|
130
|
+
### Puppeteer
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx -y agent-security-lens review puppeteer \
|
|
134
|
+
--type mcp \
|
|
135
|
+
--source-url https://github.com/modelcontextprotocol/servers \
|
|
136
|
+
--install-command "npx -y @modelcontextprotocol/server-puppeteer" \
|
|
137
|
+
--permission browser-access \
|
|
138
|
+
--permission network-access
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Observed ASL summary: `ask_user`, trust score `10`, risk level `high`, coverage `curated_baseline`.
|
|
142
|
+
|
|
143
|
+
Primary signals: `browser-access`, `network-access`, `credential-access`, `form-submission`, `shell-execution`.
|
|
144
|
+
|
|
145
|
+
### Memory
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx -y agent-security-lens review memory \
|
|
149
|
+
--type mcp \
|
|
150
|
+
--source-url https://github.com/modelcontextprotocol/servers \
|
|
151
|
+
--install-command "npx -y @modelcontextprotocol/server-memory" \
|
|
152
|
+
--permission memory-access \
|
|
153
|
+
--permission filesystem-write
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Observed ASL summary: `ask_user`, trust score `13`, risk level `medium`, coverage `unknown`.
|
|
157
|
+
|
|
158
|
+
Primary signals: `shell-execution`, `filesystem-write`, `network-access`, `unknown-source`.
|
|
159
|
+
|
|
160
|
+
## Agent Rule
|
|
161
|
+
|
|
162
|
+
Do not treat `ask_user` as failure. It means ASL found install-time exposure that requires explicit user approval, restrictions, or a reviewed alternative.
|
|
163
|
+
|
|
164
|
+
For every result, agents should execute:
|
|
165
|
+
|
|
166
|
+
1. Read `agent_decision_contract`.
|
|
167
|
+
2. Apply `safe_install_plan`.
|
|
168
|
+
3. Ask the user if `user_confirmation_required` is true.
|
|
169
|
+
4. Call `report_install_outcome` after the final install, block, skip or failure.
|
package/llms.txt
CHANGED
|
@@ -8,18 +8,23 @@ 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.
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
15
|
-
4. If `
|
|
16
|
-
5. If `
|
|
17
|
-
6.
|
|
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
|
|
21
25
|
- docs/install-snippets/README.md
|
|
22
26
|
- docs/policies/agent-preinstall-policy.md
|
|
27
|
+
- docs/public-intelligence/mcp-first-call-examples-v0.1.md
|
|
23
28
|
- docs/public-intelligence/asl-public-security-index-v0.1.md
|
|
24
29
|
- docs/public-intelligence/top-mcp-security-signals-v0.1.md
|
|
25
30
|
- docs/public-intelligence/top-skill-security-signals-v0.1.md
|
|
@@ -27,6 +32,7 @@ Public intelligence files:
|
|
|
27
32
|
- docs/public-intelligence/agent-install-decisions-v0.1.json
|
|
28
33
|
|
|
29
34
|
Important MCP tools:
|
|
35
|
+
- `get_intelligence_status`: reports online or local intelligence status for MCP clients.
|
|
30
36
|
- `review_before_install`: returns install decision, trust score, risk signals, safe install plan and alternatives.
|
|
31
37
|
- `check_component`: checks known ASL component intelligence.
|
|
32
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.
|
|
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
|
|
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.
|
|
11
|
+
"version": "0.1.9",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "agent-security-lens",
|
|
16
|
-
"version": "0.1.
|
|
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
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|