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.mjs CHANGED
@@ -1,14 +1,269 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-PY3NS2YH.mjs";
4
+ } from "./chunk-XVA2M2HS.mjs";
5
5
 
6
6
  // src/cli.ts
7
- import { readFileSync, readdirSync, statSync } from "fs";
7
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync, appendFileSync, writeFileSync } from "fs";
8
8
  import { join, relative, extname } from "path";
9
- var VERSION = "1.3.0";
9
+
10
+ // src/profile.ts
11
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readFileSync } from "fs";
12
+ var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
13
+ var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
14
+ var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
15
+ var VENDORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
16
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
17
+ ".png",
18
+ ".jpg",
19
+ ".jpeg",
20
+ ".gif",
21
+ ".webp",
22
+ ".ico",
23
+ ".bmp",
24
+ ".tif",
25
+ ".tiff",
26
+ ".avif",
27
+ ".pdf",
28
+ ".woff",
29
+ ".woff2",
30
+ ".ttf",
31
+ ".otf",
32
+ ".eot",
33
+ ".zip",
34
+ ".gz",
35
+ ".tgz",
36
+ ".tar",
37
+ ".bz2",
38
+ ".xz",
39
+ ".7z",
40
+ ".mp3",
41
+ ".mp4",
42
+ ".mov",
43
+ ".wav",
44
+ ".ogg",
45
+ ".webm",
46
+ ".wasm",
47
+ ".so",
48
+ ".dll",
49
+ ".dylib",
50
+ ".exe",
51
+ ".class",
52
+ ".jar",
53
+ ".pyc",
54
+ ".sqlite",
55
+ ".db"
56
+ ]);
57
+ var PackageError = class extends Error {
58
+ };
59
+ function displayPath(path) {
60
+ return JSON.stringify(safeText(path));
61
+ }
62
+ function safeText(text) {
63
+ let out = "";
64
+ for (const ch of text) {
65
+ const cp = ch.codePointAt(0);
66
+ 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;
67
+ out += hidden ? `\\u{${cp.toString(16).toUpperCase().padStart(4, "0")}}` : ch;
68
+ }
69
+ return out;
70
+ }
71
+ function nameProblem(name) {
72
+ for (let i = 0; i < name.length; i++) {
73
+ const c2 = name.charCodeAt(i);
74
+ if (c2 >= 55296 && c2 <= 56319) {
75
+ const next = name.charCodeAt(i + 1);
76
+ if (next >= 56320 && next <= 57343) {
77
+ i++;
78
+ continue;
79
+ }
80
+ return "contains an unpaired UTF-16 surrogate";
81
+ }
82
+ if (c2 >= 56320 && c2 <= 57343) return "contains an unpaired UTF-16 surrogate";
83
+ }
84
+ return null;
85
+ }
86
+ var strictUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
87
+ function decodeStrict(bytes) {
88
+ return strictUtf8.decode(bytes);
89
+ }
90
+ function listEntries(dir, shown) {
91
+ const entries = [];
92
+ if (process.platform === "win32") {
93
+ for (const d of readdirSync(dir, { withFileTypes: true })) {
94
+ const problem = nameProblem(d.name);
95
+ if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
96
+ entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
97
+ }
98
+ } else {
99
+ for (const d of readdirSync(dir, { withFileTypes: true, encoding: "buffer" })) {
100
+ let name;
101
+ try {
102
+ name = decodeStrict(d.name);
103
+ } catch {
104
+ throw new PackageError(`An entry in ${displayPath(shown)} has a name that is not valid UTF-8, so it cannot be sent faithfully.`);
105
+ }
106
+ entries.push({ name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
107
+ }
108
+ }
109
+ return entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
110
+ }
111
+ 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.`;
112
+ var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or directory (it is a device, socket or pipe), so it cannot be sent.`;
113
+ function readRegularFile(full, rel, budget) {
114
+ let fd;
115
+ try {
116
+ fd = openSync(full, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
117
+ } catch (e) {
118
+ const code = e.code;
119
+ if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
120
+ throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
121
+ }
122
+ try {
123
+ const st = fstatSync(fd);
124
+ if (!st.isFile()) throw new PackageError(specialMessage(rel));
125
+ 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 = readFileSync(fd);
127
+ 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
+ budget.total += bytes.length;
129
+ if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
130
+ return bytes;
131
+ } finally {
132
+ closeSync(fd);
133
+ }
134
+ }
135
+ var extensionOf = (name) => {
136
+ const i = name.lastIndexOf(".");
137
+ return i <= 0 ? "" : name.slice(i).toLowerCase();
138
+ };
139
+ function buildPackage(root) {
140
+ let rootStat;
141
+ try {
142
+ rootStat = lstatSync(root);
143
+ } catch {
144
+ throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
145
+ }
146
+ if (rootStat.isSymbolicLink()) throw new PackageError(symlinkMessage(root));
147
+ if (!rootStat.isDirectory()) throw new PackageError(`${displayPath(root)} is not a directory. A profile assessment takes a directory.`);
148
+ const files = [];
149
+ const skipped = [];
150
+ const budget = { total: 0 };
151
+ const walk = (dir, prefix, depth) => {
152
+ if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
153
+ for (const entry of listEntries(dir, prefix || ".")) {
154
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
155
+ const full = `${dir}/${entry.name}`;
156
+ if (entry.kind === "symlink") throw new PackageError(symlinkMessage(rel));
157
+ if (entry.kind === "other") throw new PackageError(specialMessage(rel));
158
+ if (entry.kind === "dir") {
159
+ if (VENDORED_DIRS.has(entry.name)) {
160
+ skipped.push({ path: rel, reason: "vendored directory" });
161
+ continue;
162
+ }
163
+ walk(full, rel, depth + 1);
164
+ continue;
165
+ }
166
+ if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
167
+ skipped.push({ path: rel, reason: "binary asset" });
168
+ continue;
169
+ }
170
+ if (files.length >= LOCAL_LIMITS.maxFiles) throw new PackageError(`The package has more than ${LOCAL_LIMITS.maxFiles} files, over the CLI's local guard.`);
171
+ const bytes = readRegularFile(full, rel, budget);
172
+ let content;
173
+ try {
174
+ content = decodeStrict(bytes);
175
+ } catch {
176
+ 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.`);
177
+ }
178
+ files.push({ path: rel, content });
179
+ }
180
+ };
181
+ walk(root, "", 0);
182
+ if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
183
+ return { files, skipped };
184
+ }
185
+ function serializeRequest(profile, files) {
186
+ return JSON.stringify({ profile, files });
187
+ }
188
+ async function postProfile(apiUrl, apiKey, body, timeoutMs) {
189
+ const controller = new AbortController();
190
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
191
+ try {
192
+ const response = await fetch(apiUrl, {
193
+ method: "POST",
194
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/1.6.0" },
195
+ body,
196
+ signal: controller.signal
197
+ });
198
+ const text = await response.text();
199
+ let json = null;
200
+ try {
201
+ json = JSON.parse(text);
202
+ } catch {
203
+ json = null;
204
+ }
205
+ return { kind: "response", status: response.status, text, json };
206
+ } catch (e) {
207
+ if (e instanceof Error && e.name === "AbortError") return { kind: "failure", message: `Request timed out after ${timeoutMs}ms` };
208
+ return { kind: "failure", message: `Request failed: ${e instanceof Error ? e.message : String(e)}` };
209
+ } finally {
210
+ clearTimeout(timer);
211
+ }
212
+ }
213
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
214
+ var str = (v) => typeof v === "string" ? safeText(v) : typeof v === "number" || typeof v === "boolean" ? String(v) : "";
215
+ function formatAssessment(body, skipped) {
216
+ const a = isObj(body.assessment) ? body.assessment : {};
217
+ const profile = isObj(a.profile) ? a.profile : {};
218
+ const pkg = isObj(a.package) ? a.package : {};
219
+ const lines = [];
220
+ lines.push("Agent Verify profile assessment");
221
+ lines.push(`Profile: ${str(body.profile)}`);
222
+ lines.push(`Framework: ${str(profile.framework)} (upstream ${str(profile.upstreamRepo)} @ ${str(profile.upstreamCommit).slice(0, 12)}, ${str(profile.upstreamStatus)}; profile version ${str(profile.agentverifyProfileVersion)})`);
223
+ lines.push(`Assessment schema: ${str(a.schemaVersion)}`);
224
+ 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)}`);
225
+ lines.push(`Package: ${str(pkg.fileCount)} files, digest ${str(pkg.digest)}`);
226
+ lines.push(`Attestation: ${body.attestation === null ? "none (this profile assessment is unsigned)" : "present (not verified by this CLI)"}`);
227
+ lines.push("");
228
+ for (const control of Array.isArray(a.controls) ? a.controls : []) {
229
+ if (!isObj(control)) continue;
230
+ const cov = isObj(control.coverage) ? control.coverage : {};
231
+ lines.push(`${str(control.controlId)} ${str(control.title)}: ${str(control.status)} (confidence ${str(control.confidence)})`);
232
+ lines.push(` ${str(cov.evidenceObserved)} evidence observed, ${str(cov.gapIdentified)} gaps identified, ${str(cov.notAssessed)} not assessed, of ${str(cov.total)} checks`);
233
+ for (const check of Array.isArray(control.checks) ? control.checks : []) {
234
+ if (isObj(check)) lines.push(` ${str(check.checkId)} ${str(check.status)}: ${str(check.title)}`);
235
+ }
236
+ lines.push("");
237
+ }
238
+ const gaps = (Array.isArray(a.evidence) ? a.evidence : []).filter((e) => isObj(e) && e.polarity === "gap");
239
+ lines.push(`Gaps identified (${gaps.length}):`);
240
+ for (const g of gaps) {
241
+ lines.push(` [${str(g.severity)}] ${str(g.kind)}: ${str(g.summary)}`);
242
+ 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)}` : ""}`);
243
+ if (locations.length) lines.push(` at ${locations.join(", ")}`);
244
+ }
245
+ if (gaps.length === 0) lines.push(" none");
246
+ const notImplemented = Array.isArray(a.notImplementedControls) ? a.notImplementedControls.map(str).join(", ") : "";
247
+ lines.push("");
248
+ lines.push(`Not assessed by this profile version: ${notImplemented || "none"}`);
249
+ for (const note of Array.isArray(a.notes) ? a.notes : []) lines.push(`Note: ${str(note)}`);
250
+ if (skipped.length > 0) {
251
+ lines.push("");
252
+ lines.push(`Skipped from the package (${skipped.length}), not sent:`);
253
+ for (const s of skipped.slice(0, 20)) lines.push(` ${displayPath(s.path)}: ${s.reason}`);
254
+ if (skipped.length > 20) lines.push(` and ${skipped.length - 20} more`);
255
+ }
256
+ return lines;
257
+ }
258
+
259
+ // src/cli.ts
260
+ var VERSION = "1.6.0";
10
261
  var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
11
262
  var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
263
+ var EXIT_OK = 0;
264
+ var EXIT_NOT_VERIFIED = 1;
265
+ var EXIT_NOT_ASSESSED = 2;
266
+ var EXIT_EXECUTION_ERROR = 3;
12
267
  var c = {
13
268
  reset: "\x1B[0m",
14
269
  bold: "\x1B[1m",
@@ -22,12 +277,13 @@ var c = {
22
277
  };
23
278
  var write = (value = "") => process.stdout.write(`${value}
24
279
  `);
25
- var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts", ".py", ".json", ".yaml", ".yml", ".md", ".mjs", ".cjs"]);
280
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
281
+ var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
26
282
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
27
283
  function collectFiles(dir) {
28
284
  const files = [];
29
285
  try {
30
- const entries = readdirSync(dir);
286
+ const entries = readdirSync2(dir);
31
287
  for (const entry of entries) {
32
288
  if (SKIP_DIRS.has(entry)) continue;
33
289
  const fullPath = join(dir, entry);
@@ -54,7 +310,10 @@ function parseArgs(argv) {
54
310
  ci: false,
55
311
  allowNotAssessed: false,
56
312
  help: false,
57
- version: false
313
+ version: false,
314
+ policy: "",
315
+ summaryFile: "",
316
+ profiles: []
58
317
  };
59
318
  if (result.command === "--help" || result.command === "-h") {
60
319
  result.command = "";
@@ -72,6 +331,10 @@ function parseArgs(argv) {
72
331
  else if (arg === "--markdown") result.markdown = true;
73
332
  else if (arg === "--ci") result.ci = true;
74
333
  else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
334
+ else if (arg === "--policy") result.policy = args[++i] ?? "";
335
+ else if (arg === "--summary-file") result.summaryFile = args[++i] ?? "";
336
+ else if (arg === "--profile") result.profiles.push(args[++i] ?? "");
337
+ else if (arg.startsWith("--profile=")) result.profiles.push(arg.slice("--profile=".length));
75
338
  else if (arg === "--help" || arg === "-h") result.help = true;
76
339
  else if (arg === "--version" || arg === "-v") result.version = true;
77
340
  else if (!arg.startsWith("-")) result.dir = arg;
@@ -99,6 +362,15 @@ ${c.bold}Options:${c.reset}
99
362
  --ci CI mode: concise output and non-zero exit for NOT VERIFIED
100
363
  --allow-not-assessed
101
364
  In CI mode, do not fail only because content is NOT_ASSESSED
365
+ --policy <id> Evaluate a built-in policy profile against this scan's evidence
366
+ in addition to the scanner's own verdict (e.g. financial-agent)
367
+ --summary-file <path>
368
+ Write results as JSON to this path (in addition to any other output)
369
+ --profile <id>
370
+ (alpha) Assess a directory against an assessment profile instead of
371
+ scanning files individually. Supported: ${SUPPORTED_PROFILES.join(", ")}
372
+ Takes a directory; not combinable with --file, --ci, --policy,
373
+ --allow-not-assessed, --markdown or --summary-file.
102
374
  --version, -v Print version
103
375
  --help, -h Show this help message
104
376
 
@@ -118,6 +390,20 @@ ${c.bold}Examples:${c.reset}
118
390
  ${c.gray}# Markdown summary for pull requests${c.reset}
119
391
  agentverify scan . --key av_your_key --markdown
120
392
 
393
+ ${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
394
+ agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
395
+
396
+ ${c.bold}Exit codes (--ci):${c.reset}
397
+ 0 All scanned files verified
398
+ 1 NOT VERIFIED \u2014 a security/verification check failed
399
+ 2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
400
+ 3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
401
+
402
+ ${c.bold}Exit codes (--profile):${c.reset}
403
+ 0 The assessment completed. Control and check statuses (including GAP_IDENTIFIED)
404
+ do not affect the exit code.
405
+ 3 Usage or execution error \u2014 nothing was assessed
406
+
121
407
  ${c.bold}Get your API key:${c.reset}
122
408
  https://aimodularity.com/agentverify/dashboard
123
409
 
@@ -139,6 +425,16 @@ function getFixFirst(result) {
139
425
  function getRelevantThreatCategories(result) {
140
426
  return (result.threatCategories ?? []).filter((item) => item.status !== "not_assessed").slice(0, 6).map((item) => `${item.label} (${item.status.replace("_", " ")})`);
141
427
  }
428
+ function criticalCount(result) {
429
+ return result.findings.filter((f) => f.severity === "critical").length;
430
+ }
431
+ function highCount(result) {
432
+ return result.findings.filter((f) => f.severity === "high").length;
433
+ }
434
+ function fingerprintPreview(result) {
435
+ const hash = result.artifactFingerprint?.artifactHash;
436
+ return hash ? `${hash.slice(0, 12)}\u2026` : null;
437
+ }
142
438
  function printDetailedSummary(result, fileName) {
143
439
  const verified = result.verdict === "VERIFIED";
144
440
  const verdictColor = verified ? c.green : result.verdict === "NOT_ASSESSED" ? c.yellow : c.red;
@@ -149,6 +445,10 @@ ${c.bold}${c.cyan}Agent Verify${c.reset}`);
149
445
  write(`${c.bold}Verdict:${c.reset} ${verdictColor}${result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : result.verdict.replace("_", " ")}${c.reset}`);
150
446
  write(`${c.bold}Risk level:${c.reset} ${result.riskLevel}`);
151
447
  write(`${c.bold}Confidence:${c.reset} ${result.confidence}/100`);
448
+ write(`${c.bold}Critical / High findings:${c.reset} ${criticalCount(result)} / ${highCount(result)}`);
449
+ if (result.metadata?.scannerVersion) write(`${c.bold}Ruleset version:${c.reset} ${result.metadata.scannerVersion}`);
450
+ if (fingerprintPreview(result)) write(`${c.bold}Artifact fingerprint:${c.reset} ${fingerprintPreview(result)}`);
451
+ if (result.policyProfile) write(`${c.bold}Policy (${result.policyProfile}):${c.reset} ${result.policyResult === "PASS" ? `${c.green}PASS${c.reset}` : `${c.red}FAIL${c.reset}`}`);
152
452
  write(`${c.bold}Top blocker:${c.reset} ${getTopBlocker(result)}`);
153
453
  write(`
154
454
  ${c.bold}Fix first:${c.reset}`);
@@ -157,6 +457,8 @@ ${c.bold}Fix first:${c.reset}`);
157
457
  else write(` No blocking fixes detected. Re-scan after any material change.`);
158
458
  if (result.reportInsights?.nextAction) write(`
159
459
  ${c.bold}Next action:${c.reset} ${result.reportInsights.nextAction}`);
460
+ const lockedFixes = (result.findings ?? []).filter((f) => f.fixLocked).length;
461
+ 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}`);
160
462
  const threats = getRelevantThreatCategories(result);
161
463
  if (threats.length) {
162
464
  write(`
@@ -175,7 +477,11 @@ function markdownSummary(results, summary) {
175
477
  lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
176
478
  continue;
177
479
  }
178
- 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)}`, "");
480
+ 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)}`);
481
+ if (item.result.metadata?.scannerVersion) lines.push(`Ruleset version: ${item.result.metadata.scannerVersion}`);
482
+ if (fingerprintPreview(item.result)) lines.push(`Artifact fingerprint: \`${fingerprintPreview(item.result)}\``);
483
+ if (item.result.policyProfile) lines.push(`Policy (${item.result.policyProfile}): ${item.result.policyResult}`);
484
+ lines.push(`Top blocker: ${getTopBlocker(item.result)}`, "");
179
485
  const fixes = getFixFirst(item.result);
180
486
  if (fixes.length) lines.push("Fix first:", ...fixes.map((fix, index) => `${index + 1}. ${fix}`), "");
181
487
  const threats = getRelevantThreatCategories(item.result);
@@ -185,6 +491,24 @@ function markdownSummary(results, summary) {
185
491
  lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
186
492
  return lines.join("\n");
187
493
  }
494
+ function writeGithubJobSummary(results, summary) {
495
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
496
+ if (!summaryPath) return;
497
+ try {
498
+ appendFileSync(summaryPath, `${markdownSummary(results, summary)}
499
+ `);
500
+ } catch (e) {
501
+ console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
502
+ }
503
+ }
504
+ function writeSummaryFile(path, results, summary) {
505
+ if (!path) return;
506
+ try {
507
+ writeFileSync(path, JSON.stringify({ results, summary }, null, 2));
508
+ } catch (e) {
509
+ console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
510
+ }
511
+ }
188
512
  function printVerdict(result, fileName) {
189
513
  const verified = result.verdict === "VERIFIED";
190
514
  const notAssessed = result.verdict === "NOT_ASSESSED";
@@ -207,7 +531,7 @@ ${c.red}Error:${c.reset} API key required
207
531
  console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/`);
208
532
  console.error(` Example: AGENTVERIFY_API_KEY=av_your_key agentverify scan .
209
533
  `);
210
- process.exit(1);
534
+ process.exit(EXIT_EXECUTION_ERROR);
211
535
  }
212
536
  let files = [];
213
537
  if (args.file) {
@@ -223,7 +547,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
223
547
  files = collectFiles(dir);
224
548
  if (files.length === 0) {
225
549
  write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
226
- write(`${c.gray}Supported: .js .ts .py .json .yaml .yml .md${c.reset}
550
+ write(`${c.gray}Supported: .js .jsx .ts .tsx .py .json .yaml .yml .toml .md .sh${c.reset}
227
551
  `);
228
552
  process.exit(0);
229
553
  }
@@ -238,10 +562,14 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
238
562
  const batch = files.slice(i, i + CONCURRENCY);
239
563
  const batchResults = await Promise.allSettled(
240
564
  batch.map(async (filePath) => {
241
- const content = readFileSync(filePath, "utf-8");
565
+ const fileStat = statSync(filePath);
566
+ if (fileStat.size > MAX_SCAN_FILE_BYTES) {
567
+ 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
+ }
569
+ const content = readFileSync2(filePath, "utf-8");
242
570
  const relPath = relative(args.dir === "." ? process.cwd() : args.dir, filePath);
243
571
  const result = await scan(
244
- { content, fileName: relPath },
572
+ { content, fileName: relPath, policyId: args.policy || void 0 },
245
573
  { apiKey: args.key, apiUrl: API_URL }
246
574
  );
247
575
  return { filePath, relPath, result };
@@ -272,21 +600,23 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
272
600
  }
273
601
  }
274
602
  const summary = { total: files.length, verified, notVerified, notAssessed, errors };
603
+ writeGithubJobSummary(jsonResults, summary);
604
+ writeSummaryFile(args.summaryFile, jsonResults, summary);
275
605
  if (args.json) {
276
606
  write(JSON.stringify({ results: jsonResults, summary }, null, 2));
277
- if (errors > 0) process.exit(1);
607
+ if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
278
608
  if (args.ci) {
279
- if (notVerified > 0) process.exit(1);
280
- if (notAssessed > 0 && !args.allowNotAssessed) process.exit(2);
609
+ if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
610
+ if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
281
611
  }
282
612
  return;
283
613
  }
284
614
  if (args.markdown) {
285
615
  write(markdownSummary(jsonResults, summary));
286
- if (errors > 0) process.exit(1);
616
+ if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
287
617
  if (args.ci) {
288
- if (notVerified > 0) process.exit(1);
289
- if (notAssessed > 0 && !args.allowNotAssessed) process.exit(2);
618
+ if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
619
+ if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
290
620
  }
291
621
  return;
292
622
  }
@@ -306,34 +636,118 @@ ${c.gray}Saved CLI reports appear in your dashboard: https://aimodularity.com/ag
306
636
  ${c.gray}No dashboard report was saved. Check API key access and retry if you need dashboard sync.${c.reset}`);
307
637
  write(`${c.gray}A2SPA docs: ${A2SPA_DOCS_URL}${c.reset}
308
638
  `);
309
- if (errors > 0) process.exit(1);
639
+ if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
310
640
  if (args.ci) {
311
- if (notVerified > 0) process.exit(1);
312
- if (notAssessed > 0 && !args.allowNotAssessed) process.exit(2);
641
+ if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
642
+ if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
643
+ }
644
+ }
645
+ var PROFILE_TIMEOUT_MS = 6e4;
646
+ function profileUsageError(message) {
647
+ console.error(`
648
+ ${c.red}Error:${c.reset} ${message}`);
649
+ console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
650
+ `);
651
+ process.exitCode = EXIT_EXECUTION_ERROR;
652
+ }
653
+ function profileFailure(json, message, detail = {}) {
654
+ if (json) write(JSON.stringify({ error: message, ...detail }, null, 2));
655
+ else console.error(`
656
+ ${c.red}Error:${c.reset} ${safeText(message)}
657
+ `);
658
+ process.exitCode = EXIT_EXECUTION_ERROR;
659
+ }
660
+ async function runProfile(args) {
661
+ if (args.profiles.length > 1) return profileUsageError("--profile was given more than once.");
662
+ const profile = args.profiles[0];
663
+ if (profile === "") return profileUsageError(`--profile requires a value. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
664
+ if (!SUPPORTED_PROFILES.includes(profile)) {
665
+ return profileUsageError(`Unknown profile ${JSON.stringify(safeText(profile))}. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
666
+ }
667
+ const conflicts = [];
668
+ if (args.file) conflicts.push("--file");
669
+ if (args.ci) conflicts.push("--ci");
670
+ if (args.allowNotAssessed) conflicts.push("--allow-not-assessed");
671
+ if (args.policy) conflicts.push("--policy");
672
+ if (args.markdown) conflicts.push("--markdown");
673
+ if (args.summaryFile) conflicts.push("--summary-file");
674
+ if (conflicts.length > 0) return profileUsageError(`--profile cannot be combined with ${conflicts.join(", ")}. A profile assessment takes a directory and reports its own result.`);
675
+ if (!args.key) {
676
+ console.error(`
677
+ ${c.red}Error:${c.reset} API key required
678
+ `);
679
+ console.error(` Set with --key av_your_key or AGENTVERIFY_API_KEY env var.`);
680
+ console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/
681
+ `);
682
+ process.exitCode = EXIT_EXECUTION_ERROR;
683
+ return;
684
+ }
685
+ let built;
686
+ try {
687
+ built = buildPackage(args.dir);
688
+ } catch (e) {
689
+ return profileFailure(args.json, e instanceof PackageError ? e.message : `Could not read the package: ${e instanceof Error ? e.message : String(e)}`);
690
+ }
691
+ if (args.json && built.skipped.length > 0) {
692
+ 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` : ""}`);
693
+ }
694
+ const result = await postProfile(API_URL, args.key, serializeRequest(profile, built.files), PROFILE_TIMEOUT_MS);
695
+ if (result.kind === "failure") return profileFailure(args.json, result.message);
696
+ const body = result.json;
697
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
698
+ if (result.status !== 200) {
699
+ if (isObject(body) && typeof body.error === "string") {
700
+ if (args.json) {
701
+ write(JSON.stringify(body, null, 2));
702
+ } else {
703
+ console.error(`
704
+ ${c.red}Error:${c.reset} ${safeText(body.error)} (HTTP ${result.status})`);
705
+ const rejection = body.rejection;
706
+ if (isObject(rejection)) console.error(` rejection: ${safeText(String(rejection.code ?? ""))}: ${safeText(String(rejection.message ?? ""))}${typeof rejection.path === "string" ? ` (${JSON.stringify(safeText(rejection.path))})` : ""}`);
707
+ if (Array.isArray(body.supportedProfiles)) console.error(` supported profiles: ${body.supportedProfiles.map((p) => safeText(String(p))).join(", ")}`);
708
+ console.error("");
709
+ }
710
+ process.exitCode = EXIT_EXECUTION_ERROR;
711
+ return;
712
+ }
713
+ return profileFailure(args.json, `Unexpected response from the service (HTTP ${result.status})`, { status: result.status });
714
+ }
715
+ if (!isObject(body) || typeof body.profile !== "string" || !isObject(body.assessment)) {
716
+ return profileFailure(args.json, "The service returned a response that is not a profile assessment", { status: result.status });
717
+ }
718
+ if (args.json) {
719
+ write(JSON.stringify(body, null, 2));
720
+ return;
721
+ }
722
+ const schema = body.assessment.schemaVersion;
723
+ if (schema !== SUPPORTED_ASSESSMENT_SCHEMA) {
724
+ 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.`);
313
725
  }
726
+ for (const line of formatAssessment(body, built.skipped)) write(line);
314
727
  }
315
728
  async function main() {
316
729
  const args = parseArgs(process.argv);
317
730
  if (args.version) {
318
731
  write(`agentverify v${VERSION}`);
319
- process.exit(0);
732
+ process.exit(EXIT_OK);
320
733
  }
321
734
  if (args.help || !args.command) {
322
735
  printHelp();
323
- process.exit(0);
736
+ process.exit(EXIT_OK);
324
737
  }
325
738
  if (args.command === "scan") {
326
- await runScan(args);
739
+ if (args.profiles.length > 0) await runProfile(args);
740
+ else await runScan(args);
327
741
  } else {
328
742
  console.error(`
329
743
  ${c.red}Unknown command: ${args.command}${c.reset}`);
330
744
  console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
331
745
  `);
332
- process.exit(1);
746
+ process.exit(EXIT_EXECUTION_ERROR);
333
747
  }
334
748
  }
335
749
  main().catch((err) => {
336
750
  console.error(`
337
751
  ${c.red}Fatal error:${c.reset}`, err instanceof Error ? err.message : err);
338
- process.exit(1);
752
+ process.exit(EXIT_EXECUTION_ERROR);
339
753
  });
package/dist/sdk.d.mts CHANGED
@@ -2,6 +2,27 @@ interface ScanInput {
2
2
  content: string;
3
3
  fileName?: string;
4
4
  platform?: string;
5
+ /** One of the built-in policy profile ids (e.g. "financial-agent") — evaluated server-side against this scan's evidence, in addition to (never instead of) the scanner's own verdict. */
6
+ policyId?: string;
7
+ }
8
+ interface ArtifactFingerprint {
9
+ artifactHash: string;
10
+ artifactHashAlgorithm: string;
11
+ artifactFingerprintVersion: string;
12
+ }
13
+ interface ReportIntegrity {
14
+ reportHash: string;
15
+ algorithm: string;
16
+ schemaVersion: string;
17
+ scannerVersion: string;
18
+ scanId: string;
19
+ timestamp: string;
20
+ }
21
+ interface SignedAttestation {
22
+ payload: Record<string, unknown>;
23
+ signature: string;
24
+ algorithm: string;
25
+ publicKey: Record<string, unknown>;
5
26
  }
6
27
  type Verdict = 'VERIFIED' | 'NOT_VERIFIED' | 'NOT_ASSESSED';
7
28
  type RiskLevel = 'Low Risk' | 'Moderate Risk' | 'High Risk';
@@ -31,6 +52,8 @@ interface Finding {
31
52
  recommendedFix: string;
32
53
  fixCode?: string;
33
54
  quickFix?: string;
55
+ /** Set by the API when this finding has a fix your plan does not include (Pro). */
56
+ fixLocked?: boolean;
34
57
  compliance?: {
35
58
  owasp?: string[];
36
59
  nist?: string[];
@@ -103,6 +126,11 @@ interface ScanResult {
103
126
  };
104
127
  saved: boolean;
105
128
  reportUrl?: string | null;
129
+ artifactFingerprint?: ArtifactFingerprint;
130
+ reportIntegrity?: ReportIntegrity;
131
+ policyProfile?: string | null;
132
+ policyResult?: string | null;
133
+ attestation?: SignedAttestation | null;
106
134
  }
107
135
  interface ScanOptions {
108
136
  apiKey: string;