agentverify 1.4.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/CHANGELOG.md CHANGED
@@ -2,7 +2,27 @@
2
2
 
3
3
  All notable changes to the `agentverify` CLI/SDK package are documented here.
4
4
 
5
- ## [1.4.0] - Unreleased
5
+ ## [1.6.0] - 2026-10-27
6
+
7
+ ### Added
8
+
9
+ - Scans now run Agent Verify's v1.6 check set (76 checks, up from 44), including framework packs
10
+ and MCP configuration checks — no CLI change needed;
11
+ results come from the API. See the root CHANGELOG for the full list.
12
+ - Directory scans now also pick up `.jsx`, `.tsx`, `.toml`, and `.sh` files.
13
+
14
+ ## [1.5.0] - 2026-09-22
15
+
16
+ ### Added
17
+
18
+ - `--profile <id>` — assess a directory against an assessment profile instead of running the
19
+ standard scan/verify flow. `owasp-agentic-skills-2026` is the first supported profile
20
+ (assessment schema `1.1.0`, currently alpha): `agentverify scan <dir> --profile owasp-agentic-skills-2026 --key av_your_key`.
21
+ Prints AST02/AST03/AST04 evidence-based results (`EVIDENCE_OBSERVED` / `GAP_IDENTIFIED`) plus
22
+ every other control explicitly as `NOT_ASSESSED`; `--json` prints the service's response
23
+ unmodified.
24
+
25
+ ## [1.4.0] - Shipped
6
26
 
7
27
  ### Changed — CI exit codes now distinguish a security failure from an execution error (behavior change)
8
28
 
package/README.md CHANGED
@@ -116,7 +116,7 @@ The workflow requires a valid `AGENTVERIFY_API_KEY`. The local Worker code path
116
116
 
117
117
  Saved CLI reports sync to the dashboard through the API `cliReports` save path. If the API cannot save the report, the CLI still prints local scan output and tells you dashboard sync did not complete.
118
118
 
119
- Verified reports can expose a README badge from the report page. A packaged Agent Verify GitHub Action (`AI-Blockchain-Ventures/agentverify@v1`) already exists and produces text/Markdown/JSON output plus an optional workflow artifact — see below. Automatically posting that output as a PR comment (rather than just producing it for you to post) is still a planned enhancement, not yet built.
119
+ Verified reports can expose a README badge from the report page. A packaged Agent Verify GitHub Action (`AI-Blockchain-Ventures/agentverify@v1.4.0`) already exists and produces text/Markdown/JSON output plus an optional workflow artifact — see below. Automatically posting that output as a PR comment (rather than just producing it for you to post) is still a planned enhancement, not yet built.
120
120
 
121
121
  See the full GitHub Action guide at `docs/github-action.md` for composite action inputs, JSON/Markdown examples, branch protection, optional PR comments, and badge guidance.
122
122
 
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
@@ -115,8 +115,257 @@ async function scan(input, options) {
115
115
  }
116
116
  }
117
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
+
118
367
  // src/cli.ts
119
- var VERSION = "1.4.0";
368
+ var VERSION = "1.6.0";
120
369
  var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
121
370
  var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
122
371
  var EXIT_OK = 0;
@@ -136,17 +385,17 @@ var c = {
136
385
  };
137
386
  var write = (value = "") => process.stdout.write(`${value}
138
387
  `);
139
- 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"]);
140
389
  var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
141
390
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
142
391
  function collectFiles(dir) {
143
392
  const files = [];
144
393
  try {
145
- const entries = (0, import_fs.readdirSync)(dir);
394
+ const entries = (0, import_fs2.readdirSync)(dir);
146
395
  for (const entry of entries) {
147
396
  if (SKIP_DIRS.has(entry)) continue;
148
397
  const fullPath = (0, import_path.join)(dir, entry);
149
- const stat = (0, import_fs.statSync)(fullPath);
398
+ const stat = (0, import_fs2.statSync)(fullPath);
150
399
  if (stat.isDirectory()) {
151
400
  files.push(...collectFiles(fullPath));
152
401
  } else if (SUPPORTED_EXTENSIONS.has((0, import_path.extname)(entry).toLowerCase())) {
@@ -171,7 +420,8 @@ function parseArgs(argv) {
171
420
  help: false,
172
421
  version: false,
173
422
  policy: "",
174
- summaryFile: ""
423
+ summaryFile: "",
424
+ profiles: []
175
425
  };
176
426
  if (result.command === "--help" || result.command === "-h") {
177
427
  result.command = "";
@@ -191,6 +441,8 @@ function parseArgs(argv) {
191
441
  else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
192
442
  else if (arg === "--policy") result.policy = args[++i] ?? "";
193
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));
194
446
  else if (arg === "--help" || arg === "-h") result.help = true;
195
447
  else if (arg === "--version" || arg === "-v") result.version = true;
196
448
  else if (!arg.startsWith("-")) result.dir = arg;
@@ -222,6 +474,11 @@ ${c.bold}Options:${c.reset}
222
474
  in addition to the scanner's own verdict (e.g. financial-agent)
223
475
  --summary-file <path>
224
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.
225
482
  --version, -v Print version
226
483
  --help, -h Show this help message
227
484
 
@@ -241,12 +498,20 @@ ${c.bold}Examples:${c.reset}
241
498
  ${c.gray}# Markdown summary for pull requests${c.reset}
242
499
  agentverify scan . --key av_your_key --markdown
243
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
+
244
504
  ${c.bold}Exit codes (--ci):${c.reset}
245
505
  0 All scanned files verified
246
506
  1 NOT VERIFIED \u2014 a security/verification check failed
247
507
  2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
248
508
  3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
249
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
+
250
515
  ${c.bold}Get your API key:${c.reset}
251
516
  https://aimodularity.com/agentverify/dashboard
252
517
 
@@ -300,6 +565,8 @@ ${c.bold}Fix first:${c.reset}`);
300
565
  else write(` No blocking fixes detected. Re-scan after any material change.`);
301
566
  if (result.reportInsights?.nextAction) write(`
302
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}`);
303
570
  const threats = getRelevantThreatCategories(result);
304
571
  if (threats.length) {
305
572
  write(`
@@ -336,7 +603,7 @@ function writeGithubJobSummary(results, summary) {
336
603
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
337
604
  if (!summaryPath) return;
338
605
  try {
339
- (0, import_fs.appendFileSync)(summaryPath, `${markdownSummary(results, summary)}
606
+ (0, import_fs2.appendFileSync)(summaryPath, `${markdownSummary(results, summary)}
340
607
  `);
341
608
  } catch (e) {
342
609
  console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
@@ -345,7 +612,7 @@ function writeGithubJobSummary(results, summary) {
345
612
  function writeSummaryFile(path, results, summary) {
346
613
  if (!path) return;
347
614
  try {
348
- (0, import_fs.writeFileSync)(path, JSON.stringify({ results, summary }, null, 2));
615
+ (0, import_fs2.writeFileSync)(path, JSON.stringify({ results, summary }, null, 2));
349
616
  } catch (e) {
350
617
  console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
351
618
  }
@@ -388,7 +655,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
388
655
  files = collectFiles(dir);
389
656
  if (files.length === 0) {
390
657
  write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
391
- 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}
392
659
  `);
393
660
  process.exit(0);
394
661
  }
@@ -403,11 +670,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
403
670
  const batch = files.slice(i, i + CONCURRENCY);
404
671
  const batchResults = await Promise.allSettled(
405
672
  batch.map(async (filePath) => {
406
- const fileStat = (0, import_fs.statSync)(filePath);
673
+ const fileStat = (0, import_fs2.statSync)(filePath);
407
674
  if (fileStat.size > MAX_SCAN_FILE_BYTES) {
408
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`);
409
676
  }
410
- const content = (0, import_fs.readFileSync)(filePath, "utf-8");
677
+ const content = (0, import_fs2.readFileSync)(filePath, "utf-8");
411
678
  const relPath = (0, import_path.relative)(args.dir === "." ? process.cwd() : args.dir, filePath);
412
679
  const result = await scan(
413
680
  { content, fileName: relPath, policyId: args.policy || void 0 },
@@ -483,6 +750,89 @@ ${c.gray}No dashboard report was saved. Check API key access and retry if you ne
483
750
  if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
484
751
  }
485
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.`);
833
+ }
834
+ for (const line of formatAssessment(body, built.skipped)) write(line);
835
+ }
486
836
  async function main() {
487
837
  const args = parseArgs(process.argv);
488
838
  if (args.version) {
@@ -494,7 +844,8 @@ async function main() {
494
844
  process.exit(EXIT_OK);
495
845
  }
496
846
  if (args.command === "scan") {
497
- await runScan(args);
847
+ if (args.profiles.length > 0) await runProfile(args);
848
+ else await runScan(args);
498
849
  } else {
499
850
  console.error(`
500
851
  ${c.red}Unknown command: ${args.command}${c.reset}`);
package/dist/cli.mjs CHANGED
@@ -4,9 +4,260 @@ import {
4
4
  } from "./chunk-XVA2M2HS.mjs";
5
5
 
6
6
  // src/cli.ts
7
- import { readFileSync, readdirSync, statSync, appendFileSync, writeFileSync } 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.4.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";
12
263
  var EXIT_OK = 0;
@@ -26,13 +277,13 @@ var c = {
26
277
  };
27
278
  var write = (value = "") => process.stdout.write(`${value}
28
279
  `);
29
- 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"]);
30
281
  var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
31
282
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
32
283
  function collectFiles(dir) {
33
284
  const files = [];
34
285
  try {
35
- const entries = readdirSync(dir);
286
+ const entries = readdirSync2(dir);
36
287
  for (const entry of entries) {
37
288
  if (SKIP_DIRS.has(entry)) continue;
38
289
  const fullPath = join(dir, entry);
@@ -61,7 +312,8 @@ function parseArgs(argv) {
61
312
  help: false,
62
313
  version: false,
63
314
  policy: "",
64
- summaryFile: ""
315
+ summaryFile: "",
316
+ profiles: []
65
317
  };
66
318
  if (result.command === "--help" || result.command === "-h") {
67
319
  result.command = "";
@@ -81,6 +333,8 @@ function parseArgs(argv) {
81
333
  else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
82
334
  else if (arg === "--policy") result.policy = args[++i] ?? "";
83
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));
84
338
  else if (arg === "--help" || arg === "-h") result.help = true;
85
339
  else if (arg === "--version" || arg === "-v") result.version = true;
86
340
  else if (!arg.startsWith("-")) result.dir = arg;
@@ -112,6 +366,11 @@ ${c.bold}Options:${c.reset}
112
366
  in addition to the scanner's own verdict (e.g. financial-agent)
113
367
  --summary-file <path>
114
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.
115
374
  --version, -v Print version
116
375
  --help, -h Show this help message
117
376
 
@@ -131,12 +390,20 @@ ${c.bold}Examples:${c.reset}
131
390
  ${c.gray}# Markdown summary for pull requests${c.reset}
132
391
  agentverify scan . --key av_your_key --markdown
133
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
+
134
396
  ${c.bold}Exit codes (--ci):${c.reset}
135
397
  0 All scanned files verified
136
398
  1 NOT VERIFIED \u2014 a security/verification check failed
137
399
  2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
138
400
  3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
139
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
+
140
407
  ${c.bold}Get your API key:${c.reset}
141
408
  https://aimodularity.com/agentverify/dashboard
142
409
 
@@ -190,6 +457,8 @@ ${c.bold}Fix first:${c.reset}`);
190
457
  else write(` No blocking fixes detected. Re-scan after any material change.`);
191
458
  if (result.reportInsights?.nextAction) write(`
192
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}`);
193
462
  const threats = getRelevantThreatCategories(result);
194
463
  if (threats.length) {
195
464
  write(`
@@ -278,7 +547,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
278
547
  files = collectFiles(dir);
279
548
  if (files.length === 0) {
280
549
  write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
281
- 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}
282
551
  `);
283
552
  process.exit(0);
284
553
  }
@@ -297,7 +566,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
297
566
  if (fileStat.size > MAX_SCAN_FILE_BYTES) {
298
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`);
299
568
  }
300
- const content = readFileSync(filePath, "utf-8");
569
+ const content = readFileSync2(filePath, "utf-8");
301
570
  const relPath = relative(args.dir === "." ? process.cwd() : args.dir, filePath);
302
571
  const result = await scan(
303
572
  { content, fileName: relPath, policyId: args.policy || void 0 },
@@ -373,6 +642,89 @@ ${c.gray}No dashboard report was saved. Check API key access and retry if you ne
373
642
  if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
374
643
  }
375
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.`);
725
+ }
726
+ for (const line of formatAssessment(body, built.skipped)) write(line);
727
+ }
376
728
  async function main() {
377
729
  const args = parseArgs(process.argv);
378
730
  if (args.version) {
@@ -384,7 +736,8 @@ async function main() {
384
736
  process.exit(EXIT_OK);
385
737
  }
386
738
  if (args.command === "scan") {
387
- await runScan(args);
739
+ if (args.profiles.length > 0) await runProfile(args);
740
+ else await runScan(args);
388
741
  } else {
389
742
  console.error(`
390
743
  ${c.red}Unknown command: ${args.command}${c.reset}`);
package/dist/sdk.d.mts CHANGED
@@ -52,6 +52,8 @@ interface Finding {
52
52
  recommendedFix: string;
53
53
  fixCode?: string;
54
54
  quickFix?: string;
55
+ /** Set by the API when this finding has a fix your plan does not include (Pro). */
56
+ fixLocked?: boolean;
55
57
  compliance?: {
56
58
  owasp?: string[];
57
59
  nist?: string[];
package/dist/sdk.d.ts CHANGED
@@ -52,6 +52,8 @@ interface Finding {
52
52
  recommendedFix: string;
53
53
  fixCode?: string;
54
54
  quickFix?: string;
55
+ /** Set by the API when this finding has a fix your plan does not include (Pro). */
56
+ fixLocked?: boolean;
55
57
  compliance?: {
56
58
  owasp?: string[];
57
59
  nist?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentverify",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "CLI and SDK for Agent Verify — execution trust analysis for AI agents",
5
5
  "bin": {
6
6
  "agentverify": "dist/cli.js"
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "tsup src/cli.ts src/sdk.ts --format cjs,esm --dts --clean",
22
22
  "dev": "tsup src/cli.ts src/sdk.ts --format cjs,esm --dts --watch",
23
- "test": "npm run build && node test/cli.test.mjs",
23
+ "test": "npm run build && node test/cli.test.mjs && node --test test/profile.test.mjs",
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "engines": { "node": ">=18.0.0" },