agentverify 1.6.0 → 2.0.0
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/dist/cli.js +431 -36
- package/dist/cli.mjs +427 -32
- package/dist/guard.d.mts +105 -0
- package/dist/guard.d.ts +105 -0
- package/dist/guard.js +249 -0
- package/dist/guard.mjs +220 -0
- package/dist/sdk.d.mts +2 -0
- package/dist/sdk.d.ts +2 -0
- package/package.json +28 -8
package/dist/cli.mjs
CHANGED
|
@@ -4,11 +4,396 @@ import {
|
|
|
4
4
|
} from "./chunk-XVA2M2HS.mjs";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
|
-
import { readFileSync as
|
|
8
|
-
import { join, relative, extname } from "path";
|
|
7
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2, appendFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { join as join2, relative as relative2, extname as extname2 } from "path";
|
|
9
|
+
|
|
10
|
+
// src/mcp.ts
|
|
11
|
+
import { createInterface } from "readline";
|
|
12
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
|
|
15
|
+
// src/files.ts
|
|
16
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "fs";
|
|
17
|
+
import { basename, extname, join, relative, sep } from "path";
|
|
18
|
+
import { homedir, platform } from "os";
|
|
19
|
+
var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
|
|
20
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache", ".venv", "venv", "__pycache__", "out", "target"]);
|
|
21
|
+
var SKIP_FILES = /* @__PURE__ */ new Set(["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "Cargo.lock", "composer.lock", "tsconfig.json"]);
|
|
22
|
+
var MAX_FILES = 60;
|
|
23
|
+
var MAX_TOTAL_BYTES = 5 * 1024 * 1024;
|
|
24
|
+
var MAX_FILE_BYTES = 400 * 1024;
|
|
25
|
+
function filePriority(relPath) {
|
|
26
|
+
const p = relPath.toLowerCase().replace(/\\/g, "/");
|
|
27
|
+
const name = p.split("/").pop() ?? p;
|
|
28
|
+
let score = 0;
|
|
29
|
+
if (/(^|\/)(tests?|__tests__|spec|e2e|fixtures?|examples?)\//.test(p) || /\.(test|spec)\.[a-z]+$/.test(name)) score -= 80;
|
|
30
|
+
if (/(agent|tool|mcp|prompt|crew|graph|server|config|policy|skill)/.test(name)) score += 30;
|
|
31
|
+
if (/(^|\/)(agents?|tools?|mcp|src|app|lib)\//.test(p)) score += 10;
|
|
32
|
+
if (name.endsWith(".md")) score -= 20;
|
|
33
|
+
if (name === "skill.md" || name === "agents.md" || name === "claude.md") score += 40;
|
|
34
|
+
return score - p.split("/").length;
|
|
35
|
+
}
|
|
36
|
+
function walk(dir, out) {
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = readdirSync(dir);
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (SKIP_DIRS.has(entry) || SKIP_FILES.has(entry)) continue;
|
|
45
|
+
const full = join(dir, entry);
|
|
46
|
+
let st;
|
|
47
|
+
try {
|
|
48
|
+
st = statSync(full);
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (st.isDirectory()) walk(full, out);
|
|
53
|
+
else if (SCANNABLE_EXTENSIONS.has(extname(entry).toLowerCase())) out.push(full);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function pickFiles(target) {
|
|
57
|
+
const notes = [];
|
|
58
|
+
let st;
|
|
59
|
+
try {
|
|
60
|
+
st = statSync(target);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(`Path not found: ${target}`);
|
|
63
|
+
}
|
|
64
|
+
if (st.isFile()) {
|
|
65
|
+
if (st.size > MAX_TOTAL_BYTES) throw new Error(`${basename(target)} is larger than 5MB, which is the most one scan can take.`);
|
|
66
|
+
return { files: [{ path: basename(target), content: readFileSync(target, "utf8") }], notes };
|
|
67
|
+
}
|
|
68
|
+
const all = [];
|
|
69
|
+
walk(target, all);
|
|
70
|
+
if (all.length === 0) throw new Error(`No agent files found in ${target}. Agent Verify reads ${[...SCANNABLE_EXTENSIONS].join(" ")} files.`);
|
|
71
|
+
const ranked = all.map((full) => ({ full, rel: relative(target, full).split(sep).join("/") })).sort((a, b) => filePriority(b.rel) - filePriority(a.rel) || a.rel.localeCompare(b.rel));
|
|
72
|
+
const files = [];
|
|
73
|
+
let total = 0;
|
|
74
|
+
let tooBig = 0;
|
|
75
|
+
for (const { full, rel } of ranked) {
|
|
76
|
+
if (files.length >= MAX_FILES) break;
|
|
77
|
+
let size = 0;
|
|
78
|
+
try {
|
|
79
|
+
size = statSync(full).size;
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (size > MAX_FILE_BYTES) {
|
|
84
|
+
tooBig++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (total + size > MAX_TOTAL_BYTES) break;
|
|
88
|
+
files.push({ path: rel, content: readFileSync(full, "utf8") });
|
|
89
|
+
total += size;
|
|
90
|
+
}
|
|
91
|
+
const left = ranked.length - files.length - tooBig;
|
|
92
|
+
if (left > 0) notes.push(`${left} lower-priority file(s) were left out to stay within one scan (${MAX_FILES} files / 5MB).`);
|
|
93
|
+
if (tooBig > 0) notes.push(`${tooBig} file(s) over 400KB were skipped.`);
|
|
94
|
+
return { files, notes };
|
|
95
|
+
}
|
|
96
|
+
function combineFiles(files) {
|
|
97
|
+
if (files.length === 1) return files[0].content;
|
|
98
|
+
return files.map((f) => `// ===== FILE: ${f.path} =====
|
|
99
|
+
${f.content}`).join("\n\n");
|
|
100
|
+
}
|
|
101
|
+
function locateLine(files, combinedLine) {
|
|
102
|
+
if (files.length === 1) return { path: files[0].path, line: combinedLine };
|
|
103
|
+
let cursor = 1;
|
|
104
|
+
for (let i = 0; i < files.length; i++) {
|
|
105
|
+
const start = cursor + 1;
|
|
106
|
+
const lines = files[i].content.split("\n").length;
|
|
107
|
+
if (combinedLine >= start && combinedLine < start + lines) return { path: files[i].path, line: combinedLine - start + 1 };
|
|
108
|
+
cursor = start + lines + 1;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function knownMcpConfigPaths(cwd = process.cwd()) {
|
|
113
|
+
const home = homedir();
|
|
114
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
115
|
+
const claudeDesktop = platform() === "win32" ? join(appData, "Claude", "claude_desktop_config.json") : platform() === "darwin" ? join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
116
|
+
return [
|
|
117
|
+
{ client: "Claude Code (this project)", path: join(cwd, ".mcp.json") },
|
|
118
|
+
{ client: "Claude Code (user)", path: join(home, ".claude.json") },
|
|
119
|
+
{ client: "Claude Desktop", path: claudeDesktop },
|
|
120
|
+
{ client: "Cursor (this project)", path: join(cwd, ".cursor", "mcp.json") },
|
|
121
|
+
{ client: "Cursor (user)", path: join(home, ".cursor", "mcp.json") },
|
|
122
|
+
{ client: "VS Code (this project)", path: join(cwd, ".vscode", "mcp.json") },
|
|
123
|
+
{ client: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json") }
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
function extractMcpServers(raw) {
|
|
127
|
+
let data;
|
|
128
|
+
try {
|
|
129
|
+
data = JSON.parse(raw);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!data || typeof data !== "object") return null;
|
|
134
|
+
const servers = {};
|
|
135
|
+
const take = (obj, prefix = "") => {
|
|
136
|
+
if (!obj || typeof obj !== "object") return;
|
|
137
|
+
for (const key of ["mcpServers", "servers"]) {
|
|
138
|
+
const block = obj[key];
|
|
139
|
+
if (block && typeof block === "object" && !Array.isArray(block)) {
|
|
140
|
+
for (const [name, def] of Object.entries(block)) servers[prefix + name] = def;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
take(data);
|
|
145
|
+
const projects = data.projects;
|
|
146
|
+
if (projects && typeof projects === "object") {
|
|
147
|
+
for (const [proj, value] of Object.entries(projects)) take(value, `${basename(proj)}/`);
|
|
148
|
+
}
|
|
149
|
+
return Object.keys(servers).length ? servers : null;
|
|
150
|
+
}
|
|
151
|
+
var SECRET_NAME = /(TOKEN|KEY|SECRET|PASSWORD|PASSWD|PAT|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
|
|
152
|
+
var SECRET_VALUE = /^(sk-|sk_|ghp_|gho_|github_pat_|xox[abp]-|AKIA|AIza|hf_|npm_|glpat-|eyJ)/;
|
|
153
|
+
function mask(value) {
|
|
154
|
+
return value.length <= 4 ? "****redacted" : `${value.slice(0, 4)}****redacted`;
|
|
155
|
+
}
|
|
156
|
+
function redactMcpSecrets(servers) {
|
|
157
|
+
const clean = (node, parentKey = "") => {
|
|
158
|
+
if (typeof node === "string") {
|
|
159
|
+
if (/^\$\{|^<|^\$[A-Z_]/.test(node)) return node;
|
|
160
|
+
if (SECRET_NAME.test(parentKey) || SECRET_VALUE.test(node)) return mask(node);
|
|
161
|
+
const flag = node.match(/^(--?[A-Za-z0-9_-]*(?:token|key|secret|password|auth)[A-Za-z0-9_-]*=)(.+)$/i);
|
|
162
|
+
if (flag) return flag[1] + mask(flag[2]);
|
|
163
|
+
return node;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(node)) return node.map((item) => clean(item, parentKey));
|
|
166
|
+
if (node && typeof node === "object") {
|
|
167
|
+
const out = {};
|
|
168
|
+
for (const [k, v] of Object.entries(node)) out[k] = clean(v, k);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
return node;
|
|
172
|
+
};
|
|
173
|
+
return clean(servers);
|
|
174
|
+
}
|
|
175
|
+
function existingMcpConfigs(cwd = process.cwd()) {
|
|
176
|
+
return knownMcpConfigPaths(cwd).filter((c2) => existsSync(c2.path));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/mcp.ts
|
|
180
|
+
var SUPPORTED_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
181
|
+
var PRICING_URL = "https://aimodularity.com/agentverify/pricing/";
|
|
182
|
+
var DASHBOARD_URL = "https://aimodularity.com/agentverify/dashboard/";
|
|
183
|
+
var TOOLS = [
|
|
184
|
+
{
|
|
185
|
+
name: "scan_path",
|
|
186
|
+
title: "Scan agent files",
|
|
187
|
+
description: "Scan an AI agent for security risks before it is deployed: dangerous tool permissions, prompt injection paths, exposed secrets, risky MCP servers, missing approval gates, SQL/SSRF/code-execution risks, and framework-specific issues (LangChain, CrewAI, AutoGen, OpenAI Agents SDK, Claude Agent SDK). Give a file or a folder; a folder is scanned as one report using the most important agent files (up to 60 files / 5MB). Returns a verdict, a 0-100 security score (higher is safer) and findings with file and line.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: { path: { type: "string", description: "File or folder to scan. Relative paths are resolved from the current project." } },
|
|
191
|
+
required: ["path"],
|
|
192
|
+
additionalProperties: false
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "scan_code",
|
|
197
|
+
title: "Scan agent code",
|
|
198
|
+
description: "Scan a snippet of agent code, a tool definition, a system prompt or an MCP config for security risks. Use this for code that is not saved to a file yet. Returns a verdict, a security score and findings.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
content: { type: "string", description: "The code or config to scan." },
|
|
203
|
+
fileName: { type: "string", description: "A file name with the right extension (e.g. agent.py, mcp.json) so the language is detected." }
|
|
204
|
+
},
|
|
205
|
+
required: ["content"],
|
|
206
|
+
additionalProperties: false
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "scan_mcp_config",
|
|
211
|
+
title: "Check my MCP servers",
|
|
212
|
+
description: "Check the MCP servers configured on this computer (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf) for risks such as broad filesystem access, unpinned packages, remote servers without auth, auto-approved tools and API keys stored in plain text. Only the server definitions are read, and secret values are masked on this computer before anything is sent.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: { path: { type: "string", description: "Optional: a specific MCP config file. Leave empty to check all known locations." } },
|
|
216
|
+
additionalProperties: false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
221
|
+
var SEVERITY_LABEL = { critical: "CRITICAL", high: "HIGH", medium: "MEDIUM", low: "LOW" };
|
|
222
|
+
function verdictLine(result) {
|
|
223
|
+
const verdict = result.verdict === "VERIFIED" ? "VERIFIED" : result.verdict === "NOT_ASSESSED" ? "NOT ASSESSED" : "NOT VERIFIED";
|
|
224
|
+
return `**Verdict: ${verdict}** \u2014 Security score ${result.riskScore}/100 (higher is safer, ${result.riskLevel})`;
|
|
225
|
+
}
|
|
226
|
+
function fixText(f) {
|
|
227
|
+
if (f.fixLocked) return `Fix: included with Agent Verify Pro (${PRICING_URL})`;
|
|
228
|
+
const steps = (f.recommendedFix ?? "").trim();
|
|
229
|
+
const code = (f.quickFix ?? f.fixCode ?? "").trim();
|
|
230
|
+
if (!steps && !code) return null;
|
|
231
|
+
return [steps ? `Fix: ${steps}` : "", code ? "```\n" + code + "\n```" : ""].filter(Boolean).join("\n");
|
|
232
|
+
}
|
|
233
|
+
function formatReport(result, opts) {
|
|
234
|
+
const findings = [...result.findings ?? []].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
|
|
235
|
+
const counts = ["critical", "high", "medium", "low"].map((s) => [s, findings.filter((f) => f.severity === s).length]).filter(([, n]) => n > 0);
|
|
236
|
+
const lines = [`# Agent Verify: ${opts.title}`, "", verdictLine(result)];
|
|
237
|
+
lines.push(findings.length ? `Findings: ${counts.map(([s, n]) => `${n} ${s}`).join(", ")}` : "No findings.");
|
|
238
|
+
if (result.reportInsights?.topBlocker) lines.push(`Fix first: ${result.reportInsights.topBlocker}`);
|
|
239
|
+
for (const note of opts.notes ?? []) lines.push(`Note: ${note}`);
|
|
240
|
+
findings.forEach((f, i) => {
|
|
241
|
+
const where = typeof f.line === "number" && f.line > 0 ? opts.files ? locateLine(opts.files, f.line) : { path: opts.title, line: f.line } : null;
|
|
242
|
+
lines.push("", `## ${i + 1}. [${SEVERITY_LABEL[f.severity] ?? f.severity}] ${f.title} (${f.code})`);
|
|
243
|
+
if (where) lines.push(`Location: ${where.path}:${where.line}`);
|
|
244
|
+
if (f.evidence) lines.push(`Evidence: ${f.evidence}`);
|
|
245
|
+
lines.push(`What is wrong: ${f.whatIsWrong}`);
|
|
246
|
+
if (f.whyItMatters) lines.push(`Why it matters: ${f.whyItMatters}`);
|
|
247
|
+
const fix = fixText(f);
|
|
248
|
+
if (fix) lines.push(fix);
|
|
249
|
+
});
|
|
250
|
+
const locked = findings.filter((f) => f.fixLocked).length;
|
|
251
|
+
if (locked > 0) lines.push("", `Step-by-step fixes and corrected code for ${locked} finding(s) are included with Pro: ${PRICING_URL}`);
|
|
252
|
+
if (result.reportUrl) lines.push("", `Full report: ${result.reportUrl}`);
|
|
253
|
+
lines.push("", "Agent Verify is an automated, evidence-based check, not a guarantee. Re-scan after making changes.");
|
|
254
|
+
return lines.join("\n");
|
|
255
|
+
}
|
|
256
|
+
function textResult(text, isError = false, structured) {
|
|
257
|
+
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {}, ...structured ? { structuredContent: structured } : {} };
|
|
258
|
+
}
|
|
259
|
+
function summary(result) {
|
|
260
|
+
return {
|
|
261
|
+
verdict: result.verdict,
|
|
262
|
+
securityScore: result.riskScore,
|
|
263
|
+
riskLevel: result.riskLevel,
|
|
264
|
+
findings: (result.findings ?? []).map((f) => ({ code: f.code, severity: f.severity, title: f.title, line: f.line ?? null })),
|
|
265
|
+
reportUrl: result.reportUrl ?? null
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function callTool(name, args, opts) {
|
|
269
|
+
const tool = TOOLS.find((t) => t.name === name);
|
|
270
|
+
if (!tool) return textResult(`Unknown tool: ${name}`, true);
|
|
271
|
+
if (!opts.apiKey) {
|
|
272
|
+
return textResult(
|
|
273
|
+
"Agent Verify needs an API key. Create a free one in the dashboard (API / CLI tab): " + DASHBOARD_URL + "\nThen set AGENTVERIFY_API_KEY in this MCP server's environment and restart it.",
|
|
274
|
+
true
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const doScan = opts.scanFn ?? scan;
|
|
278
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
279
|
+
try {
|
|
280
|
+
if (name === "scan_code") {
|
|
281
|
+
const content2 = typeof args.content === "string" ? args.content : "";
|
|
282
|
+
if (!content2.trim()) return textResult('Nothing to scan: "content" is empty.', true);
|
|
283
|
+
const fileName = typeof args.fileName === "string" && args.fileName.trim() ? args.fileName.trim() : "agent";
|
|
284
|
+
const result2 = await doScan({ content: content2, fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
|
|
285
|
+
return textResult(formatReport(result2, { title: fileName }), false, summary(result2));
|
|
286
|
+
}
|
|
287
|
+
if (name === "scan_path") {
|
|
288
|
+
const raw = typeof args.path === "string" ? args.path.trim() : "";
|
|
289
|
+
if (!raw) return textResult('Give a file or folder "path" to scan.', true);
|
|
290
|
+
const target = resolve(cwd, raw);
|
|
291
|
+
const picked = pickFiles(target);
|
|
292
|
+
const fileName = picked.files.length === 1 ? picked.files[0].path : `${raw.replace(/[\\/]+$/, "") || "project"} (${picked.files.length} files)`;
|
|
293
|
+
const result2 = await doScan({ content: combineFiles(picked.files), fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
|
|
294
|
+
const notes = [...picked.notes];
|
|
295
|
+
if (picked.files.length > 1) notes.unshift(`Scanned ${picked.files.length} files together as one report.`);
|
|
296
|
+
return textResult(formatReport(result2, { title: fileName, files: picked.files, notes }), false, summary(result2));
|
|
297
|
+
}
|
|
298
|
+
const explicit = typeof args.path === "string" && args.path.trim() ? resolve(cwd, args.path.trim()) : "";
|
|
299
|
+
const configs = explicit ? [{ client: "Config file", path: explicit }] : existingMcpConfigs(cwd);
|
|
300
|
+
if (configs.length === 0) return textResult('No MCP config files were found in the usual places (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf). Pass a "path" to check a specific file.');
|
|
301
|
+
const servers = {};
|
|
302
|
+
const read = [];
|
|
303
|
+
for (const cfg of configs) {
|
|
304
|
+
let rawText = "";
|
|
305
|
+
try {
|
|
306
|
+
rawText = readFileSync2(cfg.path, "utf8");
|
|
307
|
+
} catch {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const found = extractMcpServers(rawText);
|
|
311
|
+
if (!found) continue;
|
|
312
|
+
read.push(`${cfg.client}: ${Object.keys(found).length} server(s)`);
|
|
313
|
+
for (const [server, def] of Object.entries(found)) servers[`${cfg.client} \u203A ${server}`] = def;
|
|
314
|
+
}
|
|
315
|
+
if (Object.keys(servers).length === 0) return textResult(`Found ${configs.length} config file(s), but none of them define any MCP servers.`);
|
|
316
|
+
const content = JSON.stringify({ mcpServers: redactMcpSecrets(servers) }, null, 2);
|
|
317
|
+
const result = await doScan({ content, fileName: "mcp.json", platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
|
|
318
|
+
return textResult(
|
|
319
|
+
formatReport(result, { title: `MCP servers (${Object.keys(servers).length})`, notes: [`Checked: ${read.join("; ")}`, "Secret values were masked on this computer before scanning."] }),
|
|
320
|
+
false,
|
|
321
|
+
summary(result)
|
|
322
|
+
);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return textResult(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function handleMessage(msg, opts) {
|
|
328
|
+
const isNotification = msg.id === void 0 || msg.id === null;
|
|
329
|
+
const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: msg.id, result };
|
|
330
|
+
const fail = (code, message) => isNotification ? null : { jsonrpc: "2.0", id: msg.id ?? null, error: { code, message } };
|
|
331
|
+
switch (msg.method) {
|
|
332
|
+
case "initialize": {
|
|
333
|
+
const requested = typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : "";
|
|
334
|
+
return reply({
|
|
335
|
+
protocolVersion: SUPPORTED_PROTOCOLS.includes(requested) ? requested : SUPPORTED_PROTOCOLS[0],
|
|
336
|
+
capabilities: { tools: { listChanged: false } },
|
|
337
|
+
serverInfo: { name: "agentverify", title: "Agent Verify", version: opts.version },
|
|
338
|
+
instructions: "Use Agent Verify to check AI agent code, tools, prompts and MCP configs for security risks before deploying. Run scan_path on the agent folder after making changes, and fix critical and high findings first."
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
case "ping":
|
|
342
|
+
return reply({});
|
|
343
|
+
case "tools/list":
|
|
344
|
+
return reply({ tools: TOOLS });
|
|
345
|
+
case "tools/call": {
|
|
346
|
+
const name = typeof msg.params?.name === "string" ? msg.params.name : "";
|
|
347
|
+
const args = msg.params?.arguments && typeof msg.params.arguments === "object" ? msg.params.arguments : {};
|
|
348
|
+
return reply(await callTool(name, args, opts));
|
|
349
|
+
}
|
|
350
|
+
case "resources/list":
|
|
351
|
+
return reply({ resources: [] });
|
|
352
|
+
case "prompts/list":
|
|
353
|
+
return reply({ prompts: [] });
|
|
354
|
+
default:
|
|
355
|
+
if (typeof msg.method === "string" && msg.method.startsWith("notifications/")) return null;
|
|
356
|
+
return fail(-32601, `Method not found: ${msg.method}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function runMcpServer(opts) {
|
|
360
|
+
return new Promise((done) => {
|
|
361
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
362
|
+
const send = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
|
|
363
|
+
let pending = 0;
|
|
364
|
+
let closed = false;
|
|
365
|
+
const maybeDone = () => {
|
|
366
|
+
if (closed && pending === 0) done();
|
|
367
|
+
};
|
|
368
|
+
rl.on("line", (line) => {
|
|
369
|
+
const text = line.trim();
|
|
370
|
+
if (!text) return;
|
|
371
|
+
let msg;
|
|
372
|
+
try {
|
|
373
|
+
msg = JSON.parse(text);
|
|
374
|
+
} catch {
|
|
375
|
+
send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
pending++;
|
|
379
|
+
handleMessage(msg, opts).then((res) => {
|
|
380
|
+
if (res) send(res);
|
|
381
|
+
}).catch((err) => send({ jsonrpc: "2.0", id: msg.id ?? null, error: { code: -32603, message: err instanceof Error ? err.message : "Internal error" } })).finally(() => {
|
|
382
|
+
pending--;
|
|
383
|
+
maybeDone();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
rl.on("close", () => {
|
|
387
|
+
closed = true;
|
|
388
|
+
maybeDone();
|
|
389
|
+
});
|
|
390
|
+
process.stderr.write(`Agent Verify MCP server ${opts.version} ready${opts.apiKey ? "" : " (no API key set: tools will explain how to add one)"}
|
|
391
|
+
`);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
9
394
|
|
|
10
395
|
// src/profile.ts
|
|
11
|
-
import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readFileSync } from "fs";
|
|
396
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
12
397
|
var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
|
|
13
398
|
var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
|
|
14
399
|
var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
|
|
@@ -90,13 +475,13 @@ function decodeStrict(bytes) {
|
|
|
90
475
|
function listEntries(dir, shown) {
|
|
91
476
|
const entries = [];
|
|
92
477
|
if (process.platform === "win32") {
|
|
93
|
-
for (const d of
|
|
478
|
+
for (const d of readdirSync2(dir, { withFileTypes: true })) {
|
|
94
479
|
const problem = nameProblem(d.name);
|
|
95
480
|
if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
|
|
96
481
|
entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
97
482
|
}
|
|
98
483
|
} else {
|
|
99
|
-
for (const d of
|
|
484
|
+
for (const d of readdirSync2(dir, { withFileTypes: true, encoding: "buffer" })) {
|
|
100
485
|
let name;
|
|
101
486
|
try {
|
|
102
487
|
name = decodeStrict(d.name);
|
|
@@ -123,7 +508,7 @@ function readRegularFile(full, rel, budget) {
|
|
|
123
508
|
const st = fstatSync(fd);
|
|
124
509
|
if (!st.isFile()) throw new PackageError(specialMessage(rel));
|
|
125
510
|
if (st.size > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} is ${st.size} bytes, over the CLI's local read guard of ${LOCAL_LIMITS.maxFileBytes} bytes.`);
|
|
126
|
-
const bytes =
|
|
511
|
+
const bytes = readFileSync3(fd);
|
|
127
512
|
if (bytes.length > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} grew past the CLI's local read guard while it was being read.`);
|
|
128
513
|
budget.total += bytes.length;
|
|
129
514
|
if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
|
|
@@ -148,7 +533,7 @@ function buildPackage(root) {
|
|
|
148
533
|
const files = [];
|
|
149
534
|
const skipped = [];
|
|
150
535
|
const budget = { total: 0 };
|
|
151
|
-
const
|
|
536
|
+
const walk2 = (dir, prefix, depth) => {
|
|
152
537
|
if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
|
|
153
538
|
for (const entry of listEntries(dir, prefix || ".")) {
|
|
154
539
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
@@ -160,7 +545,7 @@ function buildPackage(root) {
|
|
|
160
545
|
skipped.push({ path: rel, reason: "vendored directory" });
|
|
161
546
|
continue;
|
|
162
547
|
}
|
|
163
|
-
|
|
548
|
+
walk2(full, rel, depth + 1);
|
|
164
549
|
continue;
|
|
165
550
|
}
|
|
166
551
|
if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
|
|
@@ -178,7 +563,7 @@ function buildPackage(root) {
|
|
|
178
563
|
files.push({ path: rel, content });
|
|
179
564
|
}
|
|
180
565
|
};
|
|
181
|
-
|
|
566
|
+
walk2(root, "", 0);
|
|
182
567
|
if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
|
|
183
568
|
return { files, skipped };
|
|
184
569
|
}
|
|
@@ -191,7 +576,7 @@ async function postProfile(apiUrl, apiKey, body, timeoutMs) {
|
|
|
191
576
|
try {
|
|
192
577
|
const response = await fetch(apiUrl, {
|
|
193
578
|
method: "POST",
|
|
194
|
-
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/
|
|
579
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/2.0.0" },
|
|
195
580
|
body,
|
|
196
581
|
signal: controller.signal
|
|
197
582
|
});
|
|
@@ -257,7 +642,7 @@ function formatAssessment(body, skipped) {
|
|
|
257
642
|
}
|
|
258
643
|
|
|
259
644
|
// src/cli.ts
|
|
260
|
-
var VERSION = "
|
|
645
|
+
var VERSION = "2.0.0";
|
|
261
646
|
var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
|
|
262
647
|
var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
|
|
263
648
|
var EXIT_OK = 0;
|
|
@@ -279,18 +664,18 @@ var write = (value = "") => process.stdout.write(`${value}
|
|
|
279
664
|
`);
|
|
280
665
|
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
|
|
281
666
|
var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
|
|
282
|
-
var
|
|
667
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
|
|
283
668
|
function collectFiles(dir) {
|
|
284
669
|
const files = [];
|
|
285
670
|
try {
|
|
286
|
-
const entries =
|
|
671
|
+
const entries = readdirSync3(dir);
|
|
287
672
|
for (const entry of entries) {
|
|
288
|
-
if (
|
|
289
|
-
const fullPath =
|
|
290
|
-
const stat =
|
|
673
|
+
if (SKIP_DIRS2.has(entry)) continue;
|
|
674
|
+
const fullPath = join2(dir, entry);
|
|
675
|
+
const stat = statSync2(fullPath);
|
|
291
676
|
if (stat.isDirectory()) {
|
|
292
677
|
files.push(...collectFiles(fullPath));
|
|
293
|
-
} else if (SUPPORTED_EXTENSIONS.has(
|
|
678
|
+
} else if (SUPPORTED_EXTENSIONS.has(extname2(entry).toLowerCase())) {
|
|
294
679
|
files.push(fullPath);
|
|
295
680
|
}
|
|
296
681
|
}
|
|
@@ -350,9 +735,12 @@ ${c.gray}Execution trust analysis for AI agents${c.reset}
|
|
|
350
735
|
${c.bold}Usage:${c.reset}
|
|
351
736
|
agentverify scan [dir] --key <api-key>
|
|
352
737
|
agentverify scan --file <file> --key <api-key>
|
|
738
|
+
agentverify mcp
|
|
353
739
|
|
|
354
740
|
${c.bold}Commands:${c.reset}
|
|
355
741
|
scan Scan agent files in a directory or a single file
|
|
742
|
+
mcp Run as an MCP server so Claude Code, Cursor, VS Code and other AI
|
|
743
|
+
tools can scan agents while you code (reads AGENTVERIFY_API_KEY)
|
|
356
744
|
|
|
357
745
|
${c.bold}Options:${c.reset}
|
|
358
746
|
--key, -k API key (or set AGENTVERIFY_API_KEY env var)
|
|
@@ -390,6 +778,9 @@ ${c.bold}Examples:${c.reset}
|
|
|
390
778
|
${c.gray}# Markdown summary for pull requests${c.reset}
|
|
391
779
|
agentverify scan . --key av_your_key --markdown
|
|
392
780
|
|
|
781
|
+
${c.gray}# Let Claude Code scan your agents (one-time setup)${c.reset}
|
|
782
|
+
claude mcp add agentverify -e AGENTVERIFY_API_KEY=av_your_key -- npx -y agentverify mcp
|
|
783
|
+
|
|
393
784
|
${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
|
|
394
785
|
agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
|
|
395
786
|
|
|
@@ -470,8 +861,8 @@ ${c.bold}Threat categories:${c.reset}`);
|
|
|
470
861
|
write(`${c.gray}Re-scan:${c.reset} agentverify scan --file ${fileName} --key $AGENTVERIFY_API_KEY
|
|
471
862
|
`);
|
|
472
863
|
}
|
|
473
|
-
function markdownSummary(results,
|
|
474
|
-
const lines = ["# Agent Verify", "", `Files scanned: ${
|
|
864
|
+
function markdownSummary(results, summary2) {
|
|
865
|
+
const lines = ["# Agent Verify", "", `Files scanned: ${summary2.total}`, `Verified: ${summary2.verified}`, `Not verified: ${summary2.notVerified}`, `Not assessed: ${summary2.notAssessed}`, `Errors: ${summary2.errors}`, ""];
|
|
475
866
|
for (const item of results) {
|
|
476
867
|
if (!item.result) {
|
|
477
868
|
lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
|
|
@@ -491,20 +882,20 @@ function markdownSummary(results, summary) {
|
|
|
491
882
|
lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
|
|
492
883
|
return lines.join("\n");
|
|
493
884
|
}
|
|
494
|
-
function writeGithubJobSummary(results,
|
|
885
|
+
function writeGithubJobSummary(results, summary2) {
|
|
495
886
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
496
887
|
if (!summaryPath) return;
|
|
497
888
|
try {
|
|
498
|
-
appendFileSync(summaryPath, `${markdownSummary(results,
|
|
889
|
+
appendFileSync(summaryPath, `${markdownSummary(results, summary2)}
|
|
499
890
|
`);
|
|
500
891
|
} catch (e) {
|
|
501
892
|
console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
|
|
502
893
|
}
|
|
503
894
|
}
|
|
504
|
-
function writeSummaryFile(path, results,
|
|
895
|
+
function writeSummaryFile(path, results, summary2) {
|
|
505
896
|
if (!path) return;
|
|
506
897
|
try {
|
|
507
|
-
writeFileSync(path, JSON.stringify({ results, summary }, null, 2));
|
|
898
|
+
writeFileSync(path, JSON.stringify({ results, summary: summary2 }, null, 2));
|
|
508
899
|
} catch (e) {
|
|
509
900
|
console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
|
|
510
901
|
}
|
|
@@ -562,12 +953,12 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
562
953
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
563
954
|
const batchResults = await Promise.allSettled(
|
|
564
955
|
batch.map(async (filePath) => {
|
|
565
|
-
const fileStat =
|
|
956
|
+
const fileStat = statSync2(filePath);
|
|
566
957
|
if (fileStat.size > MAX_SCAN_FILE_BYTES) {
|
|
567
958
|
throw new Error(`File is ${(fileStat.size / 1024 / 1024).toFixed(1)}MB, over the ${MAX_SCAN_FILE_BYTES / 1024 / 1024}MB scan limit \u2014 skipped`);
|
|
568
959
|
}
|
|
569
|
-
const content =
|
|
570
|
-
const relPath =
|
|
960
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
961
|
+
const relPath = relative2(args.dir === "." ? process.cwd() : args.dir, filePath);
|
|
571
962
|
const result = await scan(
|
|
572
963
|
{ content, fileName: relPath, policyId: args.policy || void 0 },
|
|
573
964
|
{ apiKey: args.key, apiUrl: API_URL }
|
|
@@ -589,7 +980,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
589
980
|
else notVerified++;
|
|
590
981
|
} else {
|
|
591
982
|
const filePath = batch[j];
|
|
592
|
-
const relPath =
|
|
983
|
+
const relPath = relative2(args.dir, filePath);
|
|
593
984
|
const errMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
594
985
|
if (!args.json) {
|
|
595
986
|
write(` ${c.yellow}\u26A0${c.reset} ${c.yellow}WARNING${c.reset} ${relPath.padEnd(30)} ${c.gray}${errMsg}${c.reset}`);
|
|
@@ -599,11 +990,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
599
990
|
}
|
|
600
991
|
}
|
|
601
992
|
}
|
|
602
|
-
const
|
|
603
|
-
writeGithubJobSummary(jsonResults,
|
|
604
|
-
writeSummaryFile(args.summaryFile, jsonResults,
|
|
993
|
+
const summary2 = { total: files.length, verified, notVerified, notAssessed, errors };
|
|
994
|
+
writeGithubJobSummary(jsonResults, summary2);
|
|
995
|
+
writeSummaryFile(args.summaryFile, jsonResults, summary2);
|
|
605
996
|
if (args.json) {
|
|
606
|
-
write(JSON.stringify({ results: jsonResults, summary }, null, 2));
|
|
997
|
+
write(JSON.stringify({ results: jsonResults, summary: summary2 }, null, 2));
|
|
607
998
|
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
608
999
|
if (args.ci) {
|
|
609
1000
|
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
@@ -612,7 +1003,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
612
1003
|
return;
|
|
613
1004
|
}
|
|
614
1005
|
if (args.markdown) {
|
|
615
|
-
write(markdownSummary(jsonResults,
|
|
1006
|
+
write(markdownSummary(jsonResults, summary2));
|
|
616
1007
|
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
617
1008
|
if (args.ci) {
|
|
618
1009
|
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
@@ -735,6 +1126,10 @@ async function main() {
|
|
|
735
1126
|
printHelp();
|
|
736
1127
|
process.exit(EXIT_OK);
|
|
737
1128
|
}
|
|
1129
|
+
if (args.command === "mcp") {
|
|
1130
|
+
await runMcpServer({ version: VERSION, apiKey: args.key, apiUrl: API_URL });
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
738
1133
|
if (args.command === "scan") {
|
|
739
1134
|
if (args.profiles.length > 0) await runProfile(args);
|
|
740
1135
|
else await runScan(args);
|