agentverify 1.3.0 → 1.6.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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  // src/cli.ts
5
- var import_fs = require("fs");
5
+ var import_fs2 = require("fs");
6
6
  var import_path = require("path");
7
7
 
8
8
  // src/sdk.ts
@@ -55,7 +55,12 @@ function normalizeScanResult(data) {
55
55
  scanDuration: 0
56
56
  },
57
57
  saved: data.saved === true,
58
- reportUrl: data.reportUrl ?? null
58
+ reportUrl: data.reportUrl ?? null,
59
+ artifactFingerprint: data.artifactFingerprint,
60
+ reportIntegrity: data.reportIntegrity,
61
+ policyProfile: data.policyProfile ?? null,
62
+ policyResult: data.policyResult ?? null,
63
+ attestation: data.attestation ?? null
59
64
  };
60
65
  }
61
66
  async function scan(input, options) {
@@ -81,7 +86,8 @@ async function scan(input, options) {
81
86
  body: JSON.stringify({
82
87
  content: input.content,
83
88
  fileName: input.fileName ?? "agent",
84
- platform: input.platform
89
+ platform: input.platform,
90
+ policyId: input.policyId
85
91
  }),
86
92
  signal: controller.signal
87
93
  });
@@ -109,10 +115,263 @@ async function scan(input, options) {
109
115
  }
110
116
  }
111
117
 
118
+ // src/profile.ts
119
+ var import_fs = require("fs");
120
+ var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
121
+ var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
122
+ var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
123
+ var VENDORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
124
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
125
+ ".png",
126
+ ".jpg",
127
+ ".jpeg",
128
+ ".gif",
129
+ ".webp",
130
+ ".ico",
131
+ ".bmp",
132
+ ".tif",
133
+ ".tiff",
134
+ ".avif",
135
+ ".pdf",
136
+ ".woff",
137
+ ".woff2",
138
+ ".ttf",
139
+ ".otf",
140
+ ".eot",
141
+ ".zip",
142
+ ".gz",
143
+ ".tgz",
144
+ ".tar",
145
+ ".bz2",
146
+ ".xz",
147
+ ".7z",
148
+ ".mp3",
149
+ ".mp4",
150
+ ".mov",
151
+ ".wav",
152
+ ".ogg",
153
+ ".webm",
154
+ ".wasm",
155
+ ".so",
156
+ ".dll",
157
+ ".dylib",
158
+ ".exe",
159
+ ".class",
160
+ ".jar",
161
+ ".pyc",
162
+ ".sqlite",
163
+ ".db"
164
+ ]);
165
+ var PackageError = class extends Error {
166
+ };
167
+ function displayPath(path) {
168
+ return JSON.stringify(safeText(path));
169
+ }
170
+ function safeText(text) {
171
+ let out = "";
172
+ for (const ch of text) {
173
+ const cp = ch.codePointAt(0);
174
+ const hidden = cp < 32 || cp >= 127 && cp <= 159 || cp === 173 || cp === 1564 || cp === 6158 || cp === 65279 || cp >= 8203 && cp <= 8207 || cp >= 8234 && cp <= 8238 || cp >= 8288 && cp <= 8292 || cp >= 8294 && cp <= 8297 || cp >= 917504 && cp <= 917999 || cp >= 55296 && cp <= 57343;
175
+ out += hidden ? `\\u{${cp.toString(16).toUpperCase().padStart(4, "0")}}` : ch;
176
+ }
177
+ return out;
178
+ }
179
+ function nameProblem(name) {
180
+ for (let i = 0; i < name.length; i++) {
181
+ const c2 = name.charCodeAt(i);
182
+ if (c2 >= 55296 && c2 <= 56319) {
183
+ const next = name.charCodeAt(i + 1);
184
+ if (next >= 56320 && next <= 57343) {
185
+ i++;
186
+ continue;
187
+ }
188
+ return "contains an unpaired UTF-16 surrogate";
189
+ }
190
+ if (c2 >= 56320 && c2 <= 57343) return "contains an unpaired UTF-16 surrogate";
191
+ }
192
+ return null;
193
+ }
194
+ var strictUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
195
+ function decodeStrict(bytes) {
196
+ return strictUtf8.decode(bytes);
197
+ }
198
+ function listEntries(dir, shown) {
199
+ const entries = [];
200
+ if (process.platform === "win32") {
201
+ for (const d of (0, import_fs.readdirSync)(dir, { withFileTypes: true })) {
202
+ const problem = nameProblem(d.name);
203
+ if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
204
+ entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
205
+ }
206
+ } else {
207
+ for (const d of (0, import_fs.readdirSync)(dir, { withFileTypes: true, encoding: "buffer" })) {
208
+ let name;
209
+ try {
210
+ name = decodeStrict(d.name);
211
+ } catch {
212
+ throw new PackageError(`An entry in ${displayPath(shown)} has a name that is not valid UTF-8, so it cannot be sent faithfully.`);
213
+ }
214
+ entries.push({ name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
215
+ }
216
+ }
217
+ return entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
218
+ }
219
+ var symlinkMessage = (rel) => `${displayPath(rel)} is a symbolic link. The CLI does not follow symbolic links, and a package containing one cannot be represented faithfully. Replace it with the file or directory it points to, or remove it.`;
220
+ var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or directory (it is a device, socket or pipe), so it cannot be sent.`;
221
+ function readRegularFile(full, rel, budget) {
222
+ let fd;
223
+ try {
224
+ fd = (0, import_fs.openSync)(full, import_fs.constants.O_RDONLY | (import_fs.constants.O_NOFOLLOW ?? 0));
225
+ } catch (e) {
226
+ const code = e.code;
227
+ if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
228
+ throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
229
+ }
230
+ try {
231
+ const st = (0, import_fs.fstatSync)(fd);
232
+ if (!st.isFile()) throw new PackageError(specialMessage(rel));
233
+ 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.`);
234
+ const bytes = (0, import_fs.readFileSync)(fd);
235
+ if (bytes.length > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} grew past the CLI's local read guard while it was being read.`);
236
+ budget.total += bytes.length;
237
+ if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
238
+ return bytes;
239
+ } finally {
240
+ (0, import_fs.closeSync)(fd);
241
+ }
242
+ }
243
+ var extensionOf = (name) => {
244
+ const i = name.lastIndexOf(".");
245
+ return i <= 0 ? "" : name.slice(i).toLowerCase();
246
+ };
247
+ function buildPackage(root) {
248
+ let rootStat;
249
+ try {
250
+ rootStat = (0, import_fs.lstatSync)(root);
251
+ } catch {
252
+ throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
253
+ }
254
+ if (rootStat.isSymbolicLink()) throw new PackageError(symlinkMessage(root));
255
+ if (!rootStat.isDirectory()) throw new PackageError(`${displayPath(root)} is not a directory. A profile assessment takes a directory.`);
256
+ const files = [];
257
+ const skipped = [];
258
+ const budget = { total: 0 };
259
+ const walk = (dir, prefix, depth) => {
260
+ if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
261
+ for (const entry of listEntries(dir, prefix || ".")) {
262
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
263
+ const full = `${dir}/${entry.name}`;
264
+ if (entry.kind === "symlink") throw new PackageError(symlinkMessage(rel));
265
+ if (entry.kind === "other") throw new PackageError(specialMessage(rel));
266
+ if (entry.kind === "dir") {
267
+ if (VENDORED_DIRS.has(entry.name)) {
268
+ skipped.push({ path: rel, reason: "vendored directory" });
269
+ continue;
270
+ }
271
+ walk(full, rel, depth + 1);
272
+ continue;
273
+ }
274
+ if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
275
+ skipped.push({ path: rel, reason: "binary asset" });
276
+ continue;
277
+ }
278
+ if (files.length >= LOCAL_LIMITS.maxFiles) throw new PackageError(`The package has more than ${LOCAL_LIMITS.maxFiles} files, over the CLI's local guard.`);
279
+ const bytes = readRegularFile(full, rel, budget);
280
+ let content;
281
+ try {
282
+ content = decodeStrict(bytes);
283
+ } catch {
284
+ throw new PackageError(`${displayPath(rel)} is not valid UTF-8 text, so it cannot be sent faithfully. If it is a binary asset, give it a recognized binary extension or remove it from the package.`);
285
+ }
286
+ files.push({ path: rel, content });
287
+ }
288
+ };
289
+ walk(root, "", 0);
290
+ if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
291
+ return { files, skipped };
292
+ }
293
+ function serializeRequest(profile, files) {
294
+ return JSON.stringify({ profile, files });
295
+ }
296
+ async function postProfile(apiUrl, apiKey, body, timeoutMs) {
297
+ const controller = new AbortController();
298
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
299
+ try {
300
+ const response = await fetch(apiUrl, {
301
+ method: "POST",
302
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/1.6.0" },
303
+ body,
304
+ signal: controller.signal
305
+ });
306
+ const text = await response.text();
307
+ let json = null;
308
+ try {
309
+ json = JSON.parse(text);
310
+ } catch {
311
+ json = null;
312
+ }
313
+ return { kind: "response", status: response.status, text, json };
314
+ } catch (e) {
315
+ if (e instanceof Error && e.name === "AbortError") return { kind: "failure", message: `Request timed out after ${timeoutMs}ms` };
316
+ return { kind: "failure", message: `Request failed: ${e instanceof Error ? e.message : String(e)}` };
317
+ } finally {
318
+ clearTimeout(timer);
319
+ }
320
+ }
321
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
322
+ var str = (v) => typeof v === "string" ? safeText(v) : typeof v === "number" || typeof v === "boolean" ? String(v) : "";
323
+ function formatAssessment(body, skipped) {
324
+ const a = isObj(body.assessment) ? body.assessment : {};
325
+ const profile = isObj(a.profile) ? a.profile : {};
326
+ const pkg = isObj(a.package) ? a.package : {};
327
+ const lines = [];
328
+ lines.push("Agent Verify profile assessment");
329
+ lines.push(`Profile: ${str(body.profile)}`);
330
+ lines.push(`Framework: ${str(profile.framework)} (upstream ${str(profile.upstreamRepo)} @ ${str(profile.upstreamCommit).slice(0, 12)}, ${str(profile.upstreamStatus)}; profile version ${str(profile.agentverifyProfileVersion)})`);
331
+ lines.push(`Assessment schema: ${str(a.schemaVersion)}`);
332
+ lines.push(`Interpretation: scanner ${str(profile.scannerVersion)}, engine ${str(profile.assessmentEngineVersion)}, risk rubric ${str(profile.riskRubricVersion)}, key allowlist ${str(profile.keyAllowlistVersion)}, normalization ${str(profile.normalizationVersion)}`);
333
+ lines.push(`Package: ${str(pkg.fileCount)} files, digest ${str(pkg.digest)}`);
334
+ lines.push(`Attestation: ${body.attestation === null ? "none (this profile assessment is unsigned)" : "present (not verified by this CLI)"}`);
335
+ lines.push("");
336
+ for (const control of Array.isArray(a.controls) ? a.controls : []) {
337
+ if (!isObj(control)) continue;
338
+ const cov = isObj(control.coverage) ? control.coverage : {};
339
+ lines.push(`${str(control.controlId)} ${str(control.title)}: ${str(control.status)} (confidence ${str(control.confidence)})`);
340
+ lines.push(` ${str(cov.evidenceObserved)} evidence observed, ${str(cov.gapIdentified)} gaps identified, ${str(cov.notAssessed)} not assessed, of ${str(cov.total)} checks`);
341
+ for (const check of Array.isArray(control.checks) ? control.checks : []) {
342
+ if (isObj(check)) lines.push(` ${str(check.checkId)} ${str(check.status)}: ${str(check.title)}`);
343
+ }
344
+ lines.push("");
345
+ }
346
+ const gaps = (Array.isArray(a.evidence) ? a.evidence : []).filter((e) => isObj(e) && e.polarity === "gap");
347
+ lines.push(`Gaps identified (${gaps.length}):`);
348
+ for (const g of gaps) {
349
+ lines.push(` [${str(g.severity)}] ${str(g.kind)}: ${str(g.summary)}`);
350
+ const locations = (Array.isArray(g.locations) ? g.locations : []).filter(isObj).slice(0, 3).map((l) => `${str(l.file)}${l.line !== void 0 ? `:${str(l.line)}` : ""}`);
351
+ if (locations.length) lines.push(` at ${locations.join(", ")}`);
352
+ }
353
+ if (gaps.length === 0) lines.push(" none");
354
+ const notImplemented = Array.isArray(a.notImplementedControls) ? a.notImplementedControls.map(str).join(", ") : "";
355
+ lines.push("");
356
+ lines.push(`Not assessed by this profile version: ${notImplemented || "none"}`);
357
+ for (const note of Array.isArray(a.notes) ? a.notes : []) lines.push(`Note: ${str(note)}`);
358
+ if (skipped.length > 0) {
359
+ lines.push("");
360
+ lines.push(`Skipped from the package (${skipped.length}), not sent:`);
361
+ for (const s of skipped.slice(0, 20)) lines.push(` ${displayPath(s.path)}: ${s.reason}`);
362
+ if (skipped.length > 20) lines.push(` and ${skipped.length - 20} more`);
363
+ }
364
+ return lines;
365
+ }
366
+
112
367
  // src/cli.ts
113
- var VERSION = "1.3.0";
368
+ var VERSION = "1.6.0";
114
369
  var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
115
370
  var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
371
+ var EXIT_OK = 0;
372
+ var EXIT_NOT_VERIFIED = 1;
373
+ var EXIT_NOT_ASSESSED = 2;
374
+ var EXIT_EXECUTION_ERROR = 3;
116
375
  var c = {
117
376
  reset: "\x1B[0m",
118
377
  bold: "\x1B[1m",
@@ -126,16 +385,17 @@ var c = {
126
385
  };
127
386
  var write = (value = "") => process.stdout.write(`${value}
128
387
  `);
129
- var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts", ".py", ".json", ".yaml", ".yml", ".md", ".mjs", ".cjs"]);
388
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
389
+ var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
130
390
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
131
391
  function collectFiles(dir) {
132
392
  const files = [];
133
393
  try {
134
- const entries = (0, import_fs.readdirSync)(dir);
394
+ const entries = (0, import_fs2.readdirSync)(dir);
135
395
  for (const entry of entries) {
136
396
  if (SKIP_DIRS.has(entry)) continue;
137
397
  const fullPath = (0, import_path.join)(dir, entry);
138
- const stat = (0, import_fs.statSync)(fullPath);
398
+ const stat = (0, import_fs2.statSync)(fullPath);
139
399
  if (stat.isDirectory()) {
140
400
  files.push(...collectFiles(fullPath));
141
401
  } else if (SUPPORTED_EXTENSIONS.has((0, import_path.extname)(entry).toLowerCase())) {
@@ -158,7 +418,10 @@ function parseArgs(argv) {
158
418
  ci: false,
159
419
  allowNotAssessed: false,
160
420
  help: false,
161
- version: false
421
+ version: false,
422
+ policy: "",
423
+ summaryFile: "",
424
+ profiles: []
162
425
  };
163
426
  if (result.command === "--help" || result.command === "-h") {
164
427
  result.command = "";
@@ -176,6 +439,10 @@ function parseArgs(argv) {
176
439
  else if (arg === "--markdown") result.markdown = true;
177
440
  else if (arg === "--ci") result.ci = true;
178
441
  else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
442
+ else if (arg === "--policy") result.policy = args[++i] ?? "";
443
+ else if (arg === "--summary-file") result.summaryFile = args[++i] ?? "";
444
+ else if (arg === "--profile") result.profiles.push(args[++i] ?? "");
445
+ else if (arg.startsWith("--profile=")) result.profiles.push(arg.slice("--profile=".length));
179
446
  else if (arg === "--help" || arg === "-h") result.help = true;
180
447
  else if (arg === "--version" || arg === "-v") result.version = true;
181
448
  else if (!arg.startsWith("-")) result.dir = arg;
@@ -203,6 +470,15 @@ ${c.bold}Options:${c.reset}
203
470
  --ci CI mode: concise output and non-zero exit for NOT VERIFIED
204
471
  --allow-not-assessed
205
472
  In CI mode, do not fail only because content is NOT_ASSESSED
473
+ --policy <id> Evaluate a built-in policy profile against this scan's evidence
474
+ in addition to the scanner's own verdict (e.g. financial-agent)
475
+ --summary-file <path>
476
+ Write results as JSON to this path (in addition to any other output)
477
+ --profile <id>
478
+ (alpha) Assess a directory against an assessment profile instead of
479
+ scanning files individually. Supported: ${SUPPORTED_PROFILES.join(", ")}
480
+ Takes a directory; not combinable with --file, --ci, --policy,
481
+ --allow-not-assessed, --markdown or --summary-file.
206
482
  --version, -v Print version
207
483
  --help, -h Show this help message
208
484
 
@@ -222,6 +498,20 @@ ${c.bold}Examples:${c.reset}
222
498
  ${c.gray}# Markdown summary for pull requests${c.reset}
223
499
  agentverify scan . --key av_your_key --markdown
224
500
 
501
+ ${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
502
+ agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
503
+
504
+ ${c.bold}Exit codes (--ci):${c.reset}
505
+ 0 All scanned files verified
506
+ 1 NOT VERIFIED \u2014 a security/verification check failed
507
+ 2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
508
+ 3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
509
+
510
+ ${c.bold}Exit codes (--profile):${c.reset}
511
+ 0 The assessment completed. Control and check statuses (including GAP_IDENTIFIED)
512
+ do not affect the exit code.
513
+ 3 Usage or execution error \u2014 nothing was assessed
514
+
225
515
  ${c.bold}Get your API key:${c.reset}
226
516
  https://aimodularity.com/agentverify/dashboard
227
517
 
@@ -243,6 +533,16 @@ function getFixFirst(result) {
243
533
  function getRelevantThreatCategories(result) {
244
534
  return (result.threatCategories ?? []).filter((item) => item.status !== "not_assessed").slice(0, 6).map((item) => `${item.label} (${item.status.replace("_", " ")})`);
245
535
  }
536
+ function criticalCount(result) {
537
+ return result.findings.filter((f) => f.severity === "critical").length;
538
+ }
539
+ function highCount(result) {
540
+ return result.findings.filter((f) => f.severity === "high").length;
541
+ }
542
+ function fingerprintPreview(result) {
543
+ const hash = result.artifactFingerprint?.artifactHash;
544
+ return hash ? `${hash.slice(0, 12)}\u2026` : null;
545
+ }
246
546
  function printDetailedSummary(result, fileName) {
247
547
  const verified = result.verdict === "VERIFIED";
248
548
  const verdictColor = verified ? c.green : result.verdict === "NOT_ASSESSED" ? c.yellow : c.red;
@@ -253,6 +553,10 @@ ${c.bold}${c.cyan}Agent Verify${c.reset}`);
253
553
  write(`${c.bold}Verdict:${c.reset} ${verdictColor}${result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : result.verdict.replace("_", " ")}${c.reset}`);
254
554
  write(`${c.bold}Risk level:${c.reset} ${result.riskLevel}`);
255
555
  write(`${c.bold}Confidence:${c.reset} ${result.confidence}/100`);
556
+ write(`${c.bold}Critical / High findings:${c.reset} ${criticalCount(result)} / ${highCount(result)}`);
557
+ if (result.metadata?.scannerVersion) write(`${c.bold}Ruleset version:${c.reset} ${result.metadata.scannerVersion}`);
558
+ if (fingerprintPreview(result)) write(`${c.bold}Artifact fingerprint:${c.reset} ${fingerprintPreview(result)}`);
559
+ if (result.policyProfile) write(`${c.bold}Policy (${result.policyProfile}):${c.reset} ${result.policyResult === "PASS" ? `${c.green}PASS${c.reset}` : `${c.red}FAIL${c.reset}`}`);
256
560
  write(`${c.bold}Top blocker:${c.reset} ${getTopBlocker(result)}`);
257
561
  write(`
258
562
  ${c.bold}Fix first:${c.reset}`);
@@ -261,6 +565,8 @@ ${c.bold}Fix first:${c.reset}`);
261
565
  else write(` No blocking fixes detected. Re-scan after any material change.`);
262
566
  if (result.reportInsights?.nextAction) write(`
263
567
  ${c.bold}Next action:${c.reset} ${result.reportInsights.nextAction}`);
568
+ const lockedFixes = (result.findings ?? []).filter((f) => f.fixLocked).length;
569
+ if (lockedFixes > 0) write(`${c.gray}Step-by-step fixes and corrected code for ${lockedFixes} finding${lockedFixes === 1 ? "" : "s"} are included with Pro: https://aimodularity.com/agentverify/pricing/${c.reset}`);
264
570
  const threats = getRelevantThreatCategories(result);
265
571
  if (threats.length) {
266
572
  write(`
@@ -279,7 +585,11 @@ function markdownSummary(results, summary) {
279
585
  lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
280
586
  continue;
281
587
  }
282
- lines.push(`## ${item.file}`, "", `Score: ${item.result.riskScore}/100`, `Verdict: ${item.result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : item.result.verdict.replace("_", " ")}`, `Risk level: ${item.result.riskLevel}`, `Top blocker: ${getTopBlocker(item.result)}`, "");
588
+ lines.push(`## ${item.file}`, "", `Score: ${item.result.riskScore}/100`, `Verdict: ${item.result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : item.result.verdict.replace("_", " ")}`, `Risk level: ${item.result.riskLevel}`, `Critical / High findings: ${criticalCount(item.result)} / ${highCount(item.result)}`);
589
+ if (item.result.metadata?.scannerVersion) lines.push(`Ruleset version: ${item.result.metadata.scannerVersion}`);
590
+ if (fingerprintPreview(item.result)) lines.push(`Artifact fingerprint: \`${fingerprintPreview(item.result)}\``);
591
+ if (item.result.policyProfile) lines.push(`Policy (${item.result.policyProfile}): ${item.result.policyResult}`);
592
+ lines.push(`Top blocker: ${getTopBlocker(item.result)}`, "");
283
593
  const fixes = getFixFirst(item.result);
284
594
  if (fixes.length) lines.push("Fix first:", ...fixes.map((fix, index) => `${index + 1}. ${fix}`), "");
285
595
  const threats = getRelevantThreatCategories(item.result);
@@ -289,6 +599,24 @@ function markdownSummary(results, summary) {
289
599
  lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
290
600
  return lines.join("\n");
291
601
  }
602
+ function writeGithubJobSummary(results, summary) {
603
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
604
+ if (!summaryPath) return;
605
+ try {
606
+ (0, import_fs2.appendFileSync)(summaryPath, `${markdownSummary(results, summary)}
607
+ `);
608
+ } catch (e) {
609
+ console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
610
+ }
611
+ }
612
+ function writeSummaryFile(path, results, summary) {
613
+ if (!path) return;
614
+ try {
615
+ (0, import_fs2.writeFileSync)(path, JSON.stringify({ results, summary }, null, 2));
616
+ } catch (e) {
617
+ console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
618
+ }
619
+ }
292
620
  function printVerdict(result, fileName) {
293
621
  const verified = result.verdict === "VERIFIED";
294
622
  const notAssessed = result.verdict === "NOT_ASSESSED";
@@ -311,7 +639,7 @@ ${c.red}Error:${c.reset} API key required
311
639
  console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/`);
312
640
  console.error(` Example: AGENTVERIFY_API_KEY=av_your_key agentverify scan .
313
641
  `);
314
- process.exit(1);
642
+ process.exit(EXIT_EXECUTION_ERROR);
315
643
  }
316
644
  let files = [];
317
645
  if (args.file) {
@@ -327,7 +655,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
327
655
  files = collectFiles(dir);
328
656
  if (files.length === 0) {
329
657
  write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
330
- write(`${c.gray}Supported: .js .ts .py .json .yaml .yml .md${c.reset}
658
+ write(`${c.gray}Supported: .js .jsx .ts .tsx .py .json .yaml .yml .toml .md .sh${c.reset}
331
659
  `);
332
660
  process.exit(0);
333
661
  }
@@ -342,10 +670,14 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
342
670
  const batch = files.slice(i, i + CONCURRENCY);
343
671
  const batchResults = await Promise.allSettled(
344
672
  batch.map(async (filePath) => {
345
- const content = (0, import_fs.readFileSync)(filePath, "utf-8");
673
+ const fileStat = (0, import_fs2.statSync)(filePath);
674
+ if (fileStat.size > MAX_SCAN_FILE_BYTES) {
675
+ 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`);
676
+ }
677
+ const content = (0, import_fs2.readFileSync)(filePath, "utf-8");
346
678
  const relPath = (0, import_path.relative)(args.dir === "." ? process.cwd() : args.dir, filePath);
347
679
  const result = await scan(
348
- { content, fileName: relPath },
680
+ { content, fileName: relPath, policyId: args.policy || void 0 },
349
681
  { apiKey: args.key, apiUrl: API_URL }
350
682
  );
351
683
  return { filePath, relPath, result };
@@ -376,21 +708,23 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
376
708
  }
377
709
  }
378
710
  const summary = { total: files.length, verified, notVerified, notAssessed, errors };
711
+ writeGithubJobSummary(jsonResults, summary);
712
+ writeSummaryFile(args.summaryFile, jsonResults, summary);
379
713
  if (args.json) {
380
714
  write(JSON.stringify({ results: jsonResults, summary }, null, 2));
381
- if (errors > 0) process.exit(1);
715
+ if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
382
716
  if (args.ci) {
383
- if (notVerified > 0) process.exit(1);
384
- if (notAssessed > 0 && !args.allowNotAssessed) process.exit(2);
717
+ if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
718
+ if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
385
719
  }
386
720
  return;
387
721
  }
388
722
  if (args.markdown) {
389
723
  write(markdownSummary(jsonResults, summary));
390
- if (errors > 0) process.exit(1);
724
+ if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
391
725
  if (args.ci) {
392
- if (notVerified > 0) process.exit(1);
393
- if (notAssessed > 0 && !args.allowNotAssessed) process.exit(2);
726
+ if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
727
+ if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
394
728
  }
395
729
  return;
396
730
  }
@@ -410,34 +744,118 @@ ${c.gray}Saved CLI reports appear in your dashboard: https://aimodularity.com/ag
410
744
  ${c.gray}No dashboard report was saved. Check API key access and retry if you need dashboard sync.${c.reset}`);
411
745
  write(`${c.gray}A2SPA docs: ${A2SPA_DOCS_URL}${c.reset}
412
746
  `);
413
- if (errors > 0) process.exit(1);
747
+ if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
414
748
  if (args.ci) {
415
- if (notVerified > 0) process.exit(1);
416
- if (notAssessed > 0 && !args.allowNotAssessed) process.exit(2);
749
+ if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
750
+ if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
751
+ }
752
+ }
753
+ var PROFILE_TIMEOUT_MS = 6e4;
754
+ function profileUsageError(message) {
755
+ console.error(`
756
+ ${c.red}Error:${c.reset} ${message}`);
757
+ console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
758
+ `);
759
+ process.exitCode = EXIT_EXECUTION_ERROR;
760
+ }
761
+ function profileFailure(json, message, detail = {}) {
762
+ if (json) write(JSON.stringify({ error: message, ...detail }, null, 2));
763
+ else console.error(`
764
+ ${c.red}Error:${c.reset} ${safeText(message)}
765
+ `);
766
+ process.exitCode = EXIT_EXECUTION_ERROR;
767
+ }
768
+ async function runProfile(args) {
769
+ if (args.profiles.length > 1) return profileUsageError("--profile was given more than once.");
770
+ const profile = args.profiles[0];
771
+ if (profile === "") return profileUsageError(`--profile requires a value. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
772
+ if (!SUPPORTED_PROFILES.includes(profile)) {
773
+ return profileUsageError(`Unknown profile ${JSON.stringify(safeText(profile))}. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
774
+ }
775
+ const conflicts = [];
776
+ if (args.file) conflicts.push("--file");
777
+ if (args.ci) conflicts.push("--ci");
778
+ if (args.allowNotAssessed) conflicts.push("--allow-not-assessed");
779
+ if (args.policy) conflicts.push("--policy");
780
+ if (args.markdown) conflicts.push("--markdown");
781
+ if (args.summaryFile) conflicts.push("--summary-file");
782
+ if (conflicts.length > 0) return profileUsageError(`--profile cannot be combined with ${conflicts.join(", ")}. A profile assessment takes a directory and reports its own result.`);
783
+ if (!args.key) {
784
+ console.error(`
785
+ ${c.red}Error:${c.reset} API key required
786
+ `);
787
+ console.error(` Set with --key av_your_key or AGENTVERIFY_API_KEY env var.`);
788
+ console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/
789
+ `);
790
+ process.exitCode = EXIT_EXECUTION_ERROR;
791
+ return;
792
+ }
793
+ let built;
794
+ try {
795
+ built = buildPackage(args.dir);
796
+ } catch (e) {
797
+ return profileFailure(args.json, e instanceof PackageError ? e.message : `Could not read the package: ${e instanceof Error ? e.message : String(e)}`);
798
+ }
799
+ if (args.json && built.skipped.length > 0) {
800
+ console.error(`Note: ${built.skipped.length} entries were skipped from the package and not sent: ${built.skipped.slice(0, 20).map((s) => `${displayPath(s.path)} (${s.reason})`).join(", ")}${built.skipped.length > 20 ? `, and ${built.skipped.length - 20} more` : ""}`);
801
+ }
802
+ const result = await postProfile(API_URL, args.key, serializeRequest(profile, built.files), PROFILE_TIMEOUT_MS);
803
+ if (result.kind === "failure") return profileFailure(args.json, result.message);
804
+ const body = result.json;
805
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
806
+ if (result.status !== 200) {
807
+ if (isObject(body) && typeof body.error === "string") {
808
+ if (args.json) {
809
+ write(JSON.stringify(body, null, 2));
810
+ } else {
811
+ console.error(`
812
+ ${c.red}Error:${c.reset} ${safeText(body.error)} (HTTP ${result.status})`);
813
+ const rejection = body.rejection;
814
+ if (isObject(rejection)) console.error(` rejection: ${safeText(String(rejection.code ?? ""))}: ${safeText(String(rejection.message ?? ""))}${typeof rejection.path === "string" ? ` (${JSON.stringify(safeText(rejection.path))})` : ""}`);
815
+ if (Array.isArray(body.supportedProfiles)) console.error(` supported profiles: ${body.supportedProfiles.map((p) => safeText(String(p))).join(", ")}`);
816
+ console.error("");
817
+ }
818
+ process.exitCode = EXIT_EXECUTION_ERROR;
819
+ return;
820
+ }
821
+ return profileFailure(args.json, `Unexpected response from the service (HTTP ${result.status})`, { status: result.status });
822
+ }
823
+ if (!isObject(body) || typeof body.profile !== "string" || !isObject(body.assessment)) {
824
+ return profileFailure(args.json, "The service returned a response that is not a profile assessment", { status: result.status });
825
+ }
826
+ if (args.json) {
827
+ write(JSON.stringify(body, null, 2));
828
+ return;
829
+ }
830
+ const schema = body.assessment.schemaVersion;
831
+ if (schema !== SUPPORTED_ASSESSMENT_SCHEMA) {
832
+ return profileFailure(false, `This CLI prints assessment schema ${SUPPORTED_ASSESSMENT_SCHEMA}, but the service returned ${JSON.stringify(safeText(String(schema)))}. Update the CLI, or use --json to receive the raw response.`);
417
833
  }
834
+ for (const line of formatAssessment(body, built.skipped)) write(line);
418
835
  }
419
836
  async function main() {
420
837
  const args = parseArgs(process.argv);
421
838
  if (args.version) {
422
839
  write(`agentverify v${VERSION}`);
423
- process.exit(0);
840
+ process.exit(EXIT_OK);
424
841
  }
425
842
  if (args.help || !args.command) {
426
843
  printHelp();
427
- process.exit(0);
844
+ process.exit(EXIT_OK);
428
845
  }
429
846
  if (args.command === "scan") {
430
- await runScan(args);
847
+ if (args.profiles.length > 0) await runProfile(args);
848
+ else await runScan(args);
431
849
  } else {
432
850
  console.error(`
433
851
  ${c.red}Unknown command: ${args.command}${c.reset}`);
434
852
  console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
435
853
  `);
436
- process.exit(1);
854
+ process.exit(EXIT_EXECUTION_ERROR);
437
855
  }
438
856
  }
439
857
  main().catch((err) => {
440
858
  console.error(`
441
859
  ${c.red}Fatal error:${c.reset}`, err instanceof Error ? err.message : err);
442
- process.exit(1);
860
+ process.exit(EXIT_EXECUTION_ERROR);
443
861
  });