@goplus/agentguard 1.1.8 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,20 +68,24 @@ printf '{"tool_name":"Bash","tool_input":{"command":"curl https://example.com/in
68
68
  AGENTGUARD_API_KEY=ag_live_xxxxx agentguard connect --url https://agentguard.gopluslabs.io
69
69
 
70
70
  # Optional: subscribe to AgentGuard's threat-intelligence feed. Pulls newly
71
- # published advisories from Cloud, runs a self-check against your installed
72
- # skills, and reports matches back. Run in cron / on boot.
71
+ # published advisories from Cloud and asks you to review them.
73
72
  agentguard subscribe
74
73
 
75
- # Optional: after one subscribe run, install an OpenClaw isolated cron job that
76
- # repeats the feed self-check every 15 minutes and only notifies on matches.
74
+ # Run the full quiet flow once: pull advisories, self-check local skills, and
75
+ # report local matches back to Cloud.
76
+ agentguard subscribe --quiet
77
+
78
+ # Optional: install an OpenClaw isolated cron job that checks every hour and
79
+ # asks you to review newly published advisories.
77
80
  # Requires the local OpenClaw Gateway at 127.0.0.1:18789.
78
- agentguard subscribe --install-cron
81
+ agentguard subscribe --cron "0 * * * *"
79
82
 
80
- # Override the interval if needed
81
- agentguard subscribe --install-cron --interval-minutes 5
83
+ # Or install the hourly cron in quiet mode so matches are self-checked and
84
+ # reported automatically.
85
+ agentguard subscribe --cron "0 * * * *" --quiet
82
86
 
83
87
  # Replace an existing OpenClaw cron job with the same name
84
- agentguard subscribe --install-cron --force
88
+ agentguard subscribe --cron "0 * * * *" --force
85
89
 
86
90
  # Machine-readable output always includes a cron status object:
87
91
  # cron.requested, cron.installed, and optional cron.result when installation succeeds.
package/dist/cli.js CHANGED
@@ -2,6 +2,9 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const node_fs_1 = require("node:fs");
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_path_1 = require("node:path");
7
+ const node_os_1 = require("node:os");
5
8
  const commander_1 = require("commander");
6
9
  const client_js_1 = require("./cloud/client.js");
7
10
  const config_js_1 = require("./config.js");
@@ -219,10 +222,10 @@ async function main() {
219
222
  .description('Pull new threat-feed advisories from AgentGuard Cloud and run a self-check against locally installed skills')
220
223
  .option('--since <iso>', 'Override the persisted last-pulled timestamp')
221
224
  .option('--json', 'Emit machine-readable summary instead of human text')
225
+ .option('--quiet', 'Run the full pull, self-check, and match-reporting flow with minimal output')
222
226
  .option('--no-report', 'Skip uploading self-check results back to Cloud')
223
- .option('--install-cron', 'After this run, install an OpenClaw cron job that reruns subscribe on the configured interval')
227
+ .option('--cron <expr>', 'Install an OpenClaw cron job with a five-field cron expression, for example "0 * * * *"')
224
228
  .option('--cron-name <name>', 'OpenClaw cron job name', 'agentguard-threat-feed')
225
- .option('--interval-minutes <minutes>', 'OpenClaw cron interval in minutes', '15')
226
229
  .option('--force', 'Replace an existing OpenClaw cron job with the same name')
227
230
  .option('--cron-run', 'Internal: run from the OpenClaw cron prompt without trying to install cron again')
228
231
  .action(async (options) => {
@@ -230,6 +233,10 @@ async function main() {
230
233
  const client = new client_js_1.AgentGuardCloudClient(config);
231
234
  const state = (0, state_js_1.loadFeedState)();
232
235
  const since = options.since ?? state.lastPulledAt;
236
+ const quiet = Boolean(options.quiet);
237
+ const cronExpression = options.cron && !options.cronRun
238
+ ? (0, cron_js_1.validateCronExpression)(options.cron)
239
+ : undefined;
233
240
  let advisories;
234
241
  try {
235
242
  advisories = await client.pullAdvisories(since);
@@ -244,7 +251,7 @@ async function main() {
244
251
  if (options.json) {
245
252
  console.log(JSON.stringify({ supported: false, shouldNotify: false, results: [], cron: { requested: false, installed: false } }));
246
253
  }
247
- else {
254
+ else if (!quiet) {
248
255
  console.log('AgentGuard Cloud does not expose /api/v1/feed/advisories yet — nothing to do.');
249
256
  }
250
257
  return;
@@ -259,48 +266,58 @@ async function main() {
259
266
  let cursorOk = true; // stops advancing on the first hard failure
260
267
  let latestPublishedAt = state.lastPulledAt;
261
268
  let hardFailures = 0;
262
- for (const advisory of fresh) {
263
- let processed = true;
264
- let result;
265
- try {
266
- result = await (0, selfcheck_js_1.runSelfCheckForAdvisory)(advisory);
267
- }
268
- catch (err) {
269
- // runSelfCheck shouldn't throw, but if it does the advisory has
270
- // not been evaluated — don't mark it seen and don't advance.
271
- console.error(`! Self-check threw for ${advisory.id}: ${err.message}`);
272
- hardFailures += 1;
273
- cursorOk = false;
274
- continue;
275
- }
276
- results.push(result);
277
- if (options.report !== false && client.connected && result.matchedArtifacts.length > 0) {
278
- // Report is on the critical path — if Cloud doesn't see the
279
- // match, we must NOT mark the advisory seen, otherwise a
280
- // transient network blip silently buries a real hit.
269
+ if (quiet) {
270
+ for (const advisory of fresh) {
271
+ let processed = true;
272
+ let result;
281
273
  try {
282
- await client.reportSelfCheck(advisory.id, result.matchedArtifacts, {
283
- elapsedMs: result.elapsedMs,
284
- warnings: result.warnings,
285
- });
274
+ result = await (0, selfcheck_js_1.runSelfCheckForAdvisory)(advisory);
286
275
  }
287
276
  catch (err) {
288
- console.error(`! Failed to report self-check for ${advisory.id}: ${err.message}`);
289
- processed = false;
277
+ // runSelfCheck shouldn't throw, but if it does the advisory has
278
+ // not been evaluated — don't mark it seen and don't advance.
279
+ console.error(`! Self-check threw for ${advisory.id}: ${err.message}`);
290
280
  hardFailures += 1;
281
+ cursorOk = false;
282
+ continue;
283
+ }
284
+ results.push(result);
285
+ if (options.report !== false && client.connected && result.matchedArtifacts.length > 0) {
286
+ // Report is on the critical path — if Cloud doesn't see the
287
+ // match, we must NOT mark the advisory seen, otherwise a
288
+ // transient network blip silently buries a real hit.
289
+ try {
290
+ await client.reportSelfCheck(advisory.id, result.matchedArtifacts, {
291
+ elapsedMs: result.elapsedMs,
292
+ warnings: result.warnings,
293
+ });
294
+ }
295
+ catch (err) {
296
+ console.error(`! Failed to report self-check for ${advisory.id}: ${err.message}`);
297
+ processed = false;
298
+ hardFailures += 1;
299
+ }
300
+ }
301
+ if (processed) {
302
+ Object.assign(state, (0, state_js_1.markAdvisorySeen)(state, advisory.id));
303
+ if (cursorOk && (!latestPublishedAt || advisory.publishedAt > latestPublishedAt)) {
304
+ latestPublishedAt = advisory.publishedAt;
305
+ }
306
+ }
307
+ else {
308
+ // From this point we no longer advance the pull cursor — the
309
+ // failed advisory must be re-pulled on the next run.
310
+ cursorOk = false;
291
311
  }
292
312
  }
293
- if (processed) {
313
+ }
314
+ else {
315
+ for (const advisory of fresh) {
294
316
  Object.assign(state, (0, state_js_1.markAdvisorySeen)(state, advisory.id));
295
317
  if (cursorOk && (!latestPublishedAt || advisory.publishedAt > latestPublishedAt)) {
296
318
  latestPublishedAt = advisory.publishedAt;
297
319
  }
298
320
  }
299
- else {
300
- // From this point we no longer advance the pull cursor — the
301
- // failed advisory must be re-pulled on the next run.
302
- cursorOk = false;
303
- }
304
321
  }
305
322
  state.lastPulledAt = latestPublishedAt;
306
323
  (0, state_js_1.saveFeedState)(state);
@@ -309,15 +326,18 @@ async function main() {
309
326
  supported: true,
310
327
  pulled: advisories.length,
311
328
  fresh: fresh.length,
329
+ freshAdvisories: fresh,
312
330
  results,
313
331
  hardFailures,
332
+ quiet,
314
333
  });
315
- if (options.installCron && !options.cronRun) {
334
+ if (options.cron && !options.cronRun) {
316
335
  summary.cron.requested = true;
317
336
  try {
318
337
  summary.cron.result = await (0, cron_js_1.installOpenClawThreatFeedCron)({
319
338
  name: options.cronName,
320
- intervalMinutes: (0, cron_js_1.parseIntervalMinutes)(options.intervalMinutes),
339
+ cronExpression: cronExpression,
340
+ quiet,
321
341
  force: Boolean(options.force),
322
342
  });
323
343
  summary.cron.installed = true;
@@ -331,8 +351,18 @@ async function main() {
331
351
  console.log(JSON.stringify(summary, null, 2));
332
352
  return;
333
353
  }
354
+ if (quiet && fresh.length === 0 && !summary.cron.result) {
355
+ process.exitCode = 0;
356
+ return;
357
+ }
334
358
  console.log(`Pulled ${advisories.length} advisory record(s); ${fresh.length} new.`);
335
- if (fresh.length > 0) {
359
+ if (!quiet && fresh.length > 0) {
360
+ console.log('New threat-feed advisories found. Review and handle them manually:');
361
+ for (const advisory of fresh) {
362
+ console.log(` - ${advisory.id} [${advisory.severity}] ${advisory.summary}`);
363
+ }
364
+ }
365
+ else if (quiet && fresh.length > 0) {
336
366
  console.log(`Self-check found ${totalMatches} match(es) across the new advisories.`);
337
367
  for (const r of results) {
338
368
  if (r.matchedArtifacts.length === 0)
@@ -345,8 +375,8 @@ async function main() {
345
375
  }
346
376
  if (summary.cron.result) {
347
377
  const action = summary.cron.result.created ? 'Installed' : 'OpenClaw cron job already exists';
348
- console.log(`${action} "${summary.cron.result.name}" (${summary.cron.result.schedule}).`);
349
- console.log('Notification rule: only send a message from the isolated cron session when threat-feed matches are found.');
378
+ console.log(`${action} "${summary.cron.result.name}" (${summary.cron.result.schedule}, ${summary.cron.result.timezone}).`);
379
+ console.log('Notification rule: non-quiet cron notifies on new advisories; quiet cron notifies on local matches.');
350
380
  }
351
381
  // Exit codes: 2 = matches found, 1 = at least one advisory failed
352
382
  // to evaluate or report (cursor was held back), 0 = clean.
@@ -354,7 +384,7 @@ async function main() {
354
384
  console.error(`! ${hardFailures} advisory record(s) failed to process and will be re-pulled next run.`);
355
385
  process.exitCode = 1;
356
386
  }
357
- else if (totalMatches > 0) {
387
+ else if (quiet && totalMatches > 0) {
358
388
  process.exitCode = 2;
359
389
  }
360
390
  else {
@@ -363,16 +393,38 @@ async function main() {
363
393
  });
364
394
  program
365
395
  .command('checkup')
366
- .description('Run a self-check immediately. Without --against-advisory, scans for everything in the feed cache.')
396
+ .description('Run a local agent health checkup. Use --against-advisory only for targeted threat-feed self-checks.')
367
397
  .option('--against-advisory <id>', 'Restrict the check to a single advisory id (fetches it from Cloud if needed)')
368
398
  .option('--json', 'Emit machine-readable result')
369
399
  .action(async (options) => {
370
400
  const config = (0, config_js_1.ensureConfig)();
371
- const client = new client_js_1.AgentGuardCloudClient(config);
372
401
  const advisoryId = options.againstAdvisory;
373
402
  if (!advisoryId) {
374
- console.log('Tip: pass --against-advisory <id> for now. A broader, full-fleet checkup is coming.');
375
- console.log('Meanwhile, run `agentguard subscribe` to pull the feed and self-check new entries.');
403
+ const report = await runLocalHealthCheckup(config);
404
+ if (options.json) {
405
+ console.log(JSON.stringify(report, null, 2));
406
+ }
407
+ else {
408
+ const htmlPath = await generateCheckupHtml(report).catch((err) => {
409
+ console.error(`! Could not generate visual checkup report: ${err.message}`);
410
+ return null;
411
+ });
412
+ printHealthCheckupSummary(report, htmlPath);
413
+ }
414
+ appendCheckupAudit(config.auditPath, report);
415
+ process.exitCode = 0;
416
+ return;
417
+ }
418
+ const client = new client_js_1.AgentGuardCloudClient(config);
419
+ if (!client.connected) {
420
+ const message = 'AgentGuard Cloud is not connected. Run `agentguard connect --key <key>` first.';
421
+ if (options.json) {
422
+ console.log(JSON.stringify({ success: false, error: message }, null, 2));
423
+ }
424
+ else {
425
+ console.error(message);
426
+ }
427
+ process.exitCode = 1;
376
428
  return;
377
429
  }
378
430
  let advisory = null;
@@ -418,9 +470,349 @@ function readStdinIfAvailable() {
418
470
  return '';
419
471
  }
420
472
  }
473
+ async function runLocalHealthCheckup(config) {
474
+ const skillRoots = [
475
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'skills'),
476
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'skills'),
477
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'workspace', 'skills'),
478
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.qclaw', 'skills'),
479
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.qclaw', 'workspace', 'skills'),
480
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.hermes', 'skills'),
481
+ ];
482
+ const skillDirs = discoverSkillDirs(skillRoots);
483
+ const scanner = new index_js_1.SkillScanner({ useExternalScanner: false });
484
+ const codeFindings = [];
485
+ let codeScore = skillDirs.length === 0 ? 70 : 100;
486
+ if (skillDirs.length === 0) {
487
+ codeFindings.push({ severity: 'LOW', text: 'No installed third-party skills were found to audit.' });
488
+ }
489
+ for (const dir of skillDirs) {
490
+ const result = await scanner.quickScan(dir);
491
+ const name = dir.split(/[\\/]/).pop() || dir;
492
+ if (result.risk_level === 'critical')
493
+ codeScore -= 15;
494
+ if (result.risk_level === 'high')
495
+ codeScore -= 8;
496
+ if (result.risk_level === 'medium')
497
+ codeScore -= 3;
498
+ if (result.risk_level !== 'low') {
499
+ codeFindings.push({
500
+ severity: riskLevelToSeverity(result.risk_level),
501
+ text: `${name}: ${result.summary}${result.risk_tags.length ? ` (${result.risk_tags.join(', ')})` : ''}`,
502
+ });
503
+ }
504
+ }
505
+ codeScore = clampScore(codeScore);
506
+ const credential = checkCredentialSafety(skillDirs);
507
+ const network = await checkNetworkExposure(config);
508
+ const runtime = checkRuntimeProtection(config, skillDirs.length);
509
+ const web3 = checkWeb3Safety(skillDirs);
510
+ const dimensions = {
511
+ code_safety: {
512
+ score: codeScore,
513
+ findings: codeFindings,
514
+ details: `${skillDirs.length} installed skill(s) scanned with AgentGuard rules.`,
515
+ },
516
+ credential_safety: credential,
517
+ network_exposure: network,
518
+ runtime_protection: runtime,
519
+ web3_safety: web3,
520
+ };
521
+ const composite = calculateCompositeScore(dimensions);
522
+ const recommendations = Object.values(dimensions)
523
+ .flatMap((d) => d.findings)
524
+ .filter((f) => f.severity !== 'LOW')
525
+ .slice(0, 8);
526
+ return {
527
+ timestamp: new Date().toISOString(),
528
+ composite_score: composite,
529
+ tier: tierForScore(composite),
530
+ dimensions,
531
+ skills_scanned: skillDirs.length,
532
+ protection_level: config.level,
533
+ analysis: buildHealthAnalysis(composite, dimensions),
534
+ recommendations,
535
+ };
536
+ }
537
+ function discoverSkillDirs(roots) {
538
+ const dirs = [];
539
+ for (const root of roots) {
540
+ if (!(0, node_fs_1.existsSync)(root))
541
+ continue;
542
+ let entries;
543
+ try {
544
+ entries = (0, node_fs_1.readdirSync)(root, { withFileTypes: true });
545
+ }
546
+ catch {
547
+ continue;
548
+ }
549
+ for (const entry of entries) {
550
+ if (!entry.isDirectory())
551
+ continue;
552
+ const dir = (0, node_path_1.join)(root, entry.name);
553
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'SKILL.md')))
554
+ dirs.push(dir);
555
+ }
556
+ }
557
+ return dirs;
558
+ }
559
+ function checkCredentialSafety(skillDirs) {
560
+ let score = 100;
561
+ const findings = [];
562
+ for (const [path, severity] of [
563
+ [(0, node_path_1.join)((0, node_os_1.homedir)(), '.ssh'), 'HIGH'],
564
+ [(0, node_path_1.join)((0, node_os_1.homedir)(), '.gnupg'), 'MEDIUM'],
565
+ ]) {
566
+ const mode = permissionMode(path);
567
+ if (mode !== null && mode > 0o700) {
568
+ score -= severity === 'HIGH' ? 25 : 15;
569
+ findings.push({ severity, text: `${path} permissions are ${mode.toString(8)}; expected 700 or stricter.` });
570
+ }
571
+ }
572
+ const secretPatterns = [
573
+ { re: /0x[a-fA-F0-9]{64}|-----BEGIN [A-Z ]*PRIVATE KEY-----/, severity: 'CRITICAL', label: 'Plaintext private key pattern' },
574
+ { re: /\b(seed_phrase|mnemonic)\b/i, severity: 'CRITICAL', label: 'Mnemonic or seed phrase marker' },
575
+ { re: /\bAKIA[0-9A-Z]{16}\b|\bgh[pousr]_[A-Za-z0-9_]{20,}\b/, severity: 'HIGH', label: 'API key or token pattern' },
576
+ ];
577
+ for (const dir of skillDirs) {
578
+ const manifest = (0, node_path_1.join)(dir, 'SKILL.md');
579
+ if (!(0, node_fs_1.existsSync)(manifest))
580
+ continue;
581
+ let body = '';
582
+ try {
583
+ body = (0, node_fs_1.readFileSync)(manifest, 'utf8').slice(0, 256 * 1024);
584
+ }
585
+ catch {
586
+ continue;
587
+ }
588
+ for (const pattern of secretPatterns) {
589
+ if (!pattern.re.test(body))
590
+ continue;
591
+ score -= pattern.severity === 'CRITICAL' ? 25 : 15;
592
+ findings.push({ severity: pattern.severity, text: `${pattern.label} found in ${manifest}.` });
593
+ }
594
+ }
595
+ return {
596
+ score: clampScore(score),
597
+ findings,
598
+ details: findings.length ? `${findings.length} credential hygiene issue(s) found.` : 'Credential permissions and scanned manifests look clean.',
599
+ };
600
+ }
601
+ async function checkNetworkExposure(config) {
602
+ let score = 100;
603
+ const findings = [];
604
+ const listeners = await runCommandText('lsof', ['-i', '-P', '-n']);
605
+ for (const port of ['2375', '3306', '6379', '27017']) {
606
+ const exposed = new RegExp(`(\\*|0\\.0\\.0\\.0):${port}\\b`).test(listeners);
607
+ if (exposed) {
608
+ score -= 25;
609
+ findings.push({ severity: 'HIGH', text: `High-risk service appears exposed on 0.0.0.0:${port}.` });
610
+ }
611
+ }
612
+ const cron = await runCommandText('crontab', ['-l']);
613
+ if (/(curl\b.*\|\s*(bash|sh)|wget\b.*\|\s*(bash|sh)|\.ssh)/i.test(cron)) {
614
+ score -= 30;
615
+ findings.push({ severity: 'HIGH', text: 'Suspicious cron entry found that downloads shell code or accesses SSH material.' });
616
+ }
617
+ const sensitiveEnvNames = Object.keys(process.env).filter((name) => /PRIVATE_KEY|MNEMONIC|SECRET|PASSWORD/i.test(name));
618
+ if (sensitiveEnvNames.length > 0) {
619
+ score -= 20;
620
+ findings.push({ severity: 'MEDIUM', text: `Sensitive environment variable names are present: ${sensitiveEnvNames.slice(0, 8).join(', ')}.` });
621
+ }
622
+ for (const path of [(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'openclaw.json'), (0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'devices', 'paired.json')]) {
623
+ const mode = permissionMode(path);
624
+ if (mode !== null && mode > 0o600) {
625
+ score -= 15;
626
+ findings.push({ severity: 'MEDIUM', text: `${path} permissions are ${mode.toString(8)}; expected 600 or stricter.` });
627
+ }
628
+ }
629
+ return {
630
+ score: clampScore(score),
631
+ findings,
632
+ details: findings.length ? `${findings.length} network/system exposure issue(s) found.` : 'No dangerous ports, cron entries, or config permission issues found.',
633
+ };
634
+ }
635
+ function checkRuntimeProtection(config, skillsScanned) {
636
+ let score = 0;
637
+ const findings = [];
638
+ const hookFiles = [
639
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'settings.json'),
640
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'openclaw.json'),
641
+ (0, node_path_1.join)((0, node_os_1.homedir)(), '.hermes', 'config.yaml'),
642
+ ];
643
+ const hasHook = hookFiles.some((path) => {
644
+ if (!(0, node_fs_1.existsSync)(path))
645
+ return false;
646
+ try {
647
+ return /agentguard|guard-hook|hermes-hook/i.test((0, node_fs_1.readFileSync)(path, 'utf8'));
648
+ }
649
+ catch {
650
+ return false;
651
+ }
652
+ });
653
+ if (hasHook)
654
+ score += 40;
655
+ else
656
+ findings.push({ severity: 'HIGH', text: 'No AgentGuard runtime hook was detected in known agent configuration files.' });
657
+ if ((0, node_fs_1.existsSync)(config.auditPath))
658
+ score += 30;
659
+ else
660
+ findings.push({ severity: 'MEDIUM', text: 'No AgentGuard audit log exists yet, so runtime threat history is unavailable.' });
661
+ if (skillsScanned > 0)
662
+ score += 30;
663
+ else
664
+ findings.push({ severity: 'MEDIUM', text: 'No installed skills were scanned during this checkup.' });
665
+ return {
666
+ score: clampScore(score),
667
+ findings,
668
+ details: findings.length ? `${findings.length} runtime protection gap(s) found.` : 'Runtime hooks, audit logging, and skill scanning are present.',
669
+ };
670
+ }
671
+ function checkWeb3Safety(skillDirs) {
672
+ const web3Detected = ['GOPLUS_API_KEY', 'CHAIN_ID', 'RPC_URL'].some((name) => process.env[name]) ||
673
+ skillDirs.some((dir) => /web3|wallet|chain|defi|token/i.test(dir));
674
+ if (!web3Detected) {
675
+ return { score: null, na: true, findings: [], details: 'No Web3 usage detected.' };
676
+ }
677
+ const findings = [];
678
+ let score = process.env.GOPLUS_API_KEY ? 100 : 70;
679
+ if (!process.env.GOPLUS_API_KEY) {
680
+ findings.push({ severity: 'MEDIUM', text: 'Web3 usage detected but GOPLUS_API_KEY is not configured for transaction checks.' });
681
+ }
682
+ return {
683
+ score,
684
+ findings,
685
+ details: findings.length ? 'Web3 usage detected with missing transaction security configuration.' : 'Web3 safety configuration is present.',
686
+ };
687
+ }
688
+ function permissionMode(path) {
689
+ if (!(0, node_fs_1.existsSync)(path))
690
+ return null;
691
+ try {
692
+ return (0, node_fs_1.statSync)(path).mode & 0o777;
693
+ }
694
+ catch {
695
+ return null;
696
+ }
697
+ }
698
+ function runCommandText(command, args) {
699
+ return new Promise((resolvePromise) => {
700
+ try {
701
+ const child = (0, node_child_process_1.execFile)(command, args, { timeout: 2000 }, (error, stdout, stderr) => {
702
+ if (error)
703
+ resolvePromise('');
704
+ else
705
+ resolvePromise(`${stdout || ''}${stderr || ''}`);
706
+ });
707
+ child.on('error', () => resolvePromise(''));
708
+ }
709
+ catch {
710
+ resolvePromise('');
711
+ }
712
+ });
713
+ }
714
+ function calculateCompositeScore(dimensions) {
715
+ const web3Score = dimensions.web3_safety.score;
716
+ if (web3Score === null || dimensions.web3_safety.na) {
717
+ return Math.round((dimensions.code_safety.score ?? 0) * 0.294 +
718
+ (dimensions.credential_safety.score ?? 0) * 0.294 +
719
+ (dimensions.network_exposure.score ?? 0) * 0.235 +
720
+ (dimensions.runtime_protection.score ?? 0) * 0.176);
721
+ }
722
+ return Math.round((dimensions.code_safety.score ?? 0) * 0.25 +
723
+ (dimensions.credential_safety.score ?? 0) * 0.25 +
724
+ (dimensions.network_exposure.score ?? 0) * 0.20 +
725
+ (dimensions.runtime_protection.score ?? 0) * 0.15 +
726
+ web3Score * 0.15);
727
+ }
728
+ async function generateCheckupHtml(report) {
729
+ const tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentguard-checkup-'));
730
+ const dataPath = (0, node_path_1.join)(tempDir, 'data.json');
731
+ (0, node_fs_1.writeFileSync)(dataPath, JSON.stringify(report, null, 2), 'utf8');
732
+ const scriptPath = process.env.AGENTGUARD_CHECKUP_REPORT_SCRIPT
733
+ ? (0, node_path_1.resolve)(process.env.AGENTGUARD_CHECKUP_REPORT_SCRIPT)
734
+ : (0, node_path_1.resolve)(__dirname, '..', 'skills', 'agentguard', 'scripts', 'checkup-report.js');
735
+ if (!(0, node_fs_1.existsSync)(scriptPath)) {
736
+ throw new Error(`report generator not found at ${scriptPath}`);
737
+ }
738
+ return new Promise((resolvePromise, reject) => {
739
+ (0, node_child_process_1.execFile)('node', [scriptPath, '--file', dataPath], { timeout: 6000 }, (error, stdout, stderr) => {
740
+ if (error)
741
+ reject(new Error(stderr || error.message));
742
+ else
743
+ resolvePromise(stdout.trim().split(/\r?\n/).pop() || '');
744
+ });
745
+ });
746
+ }
747
+ function printHealthCheckupSummary(report, htmlPath) {
748
+ const totalFindings = Object.values(report.dimensions).reduce((sum, dim) => sum + dim.findings.length, 0);
749
+ console.log('AgentGuard Health Checkup');
750
+ console.log(`Overall Health Score: ${report.composite_score}/100 (Tier ${report.tier})`);
751
+ console.log(`Findings: ${totalFindings}`);
752
+ console.log(`Skills scanned: ${report.skills_scanned}`);
753
+ for (const [name, dim] of Object.entries(report.dimensions)) {
754
+ const score = dim.na || dim.score === null ? 'N/A' : `${dim.score}/100`;
755
+ console.log(`- ${name}: ${score} - ${dim.details}`);
756
+ }
757
+ if (htmlPath) {
758
+ console.log(`Full visual report: ${htmlPath}`);
759
+ }
760
+ else {
761
+ console.log('Full visual report: unavailable (text summary shown above)');
762
+ }
763
+ }
764
+ function appendCheckupAudit(auditPath, report) {
765
+ const totalFindings = Object.values(report.dimensions).reduce((sum, dim) => sum + dim.findings.length, 0);
766
+ try {
767
+ (0, node_fs_1.appendFileSync)(auditPath, `${JSON.stringify({
768
+ timestamp: report.timestamp,
769
+ event: 'checkup',
770
+ composite_score: report.composite_score,
771
+ tier: report.tier,
772
+ checks: 5,
773
+ findings: totalFindings,
774
+ skills_scanned: report.skills_scanned,
775
+ })}\n`, { mode: 0o600 });
776
+ }
777
+ catch {
778
+ // Checkup should still succeed if audit logging is unavailable.
779
+ }
780
+ }
781
+ function buildHealthAnalysis(score, dimensions) {
782
+ const weak = Object.entries(dimensions)
783
+ .filter(([, dim]) => !dim.na && dim.score !== null && dim.score < 70)
784
+ .map(([name]) => name.replace(/_/g, ' '));
785
+ if (weak.length === 0) {
786
+ return `Overall posture is healthy at ${score}/100. AgentGuard did not find major weaknesses across the local skill, credential, network, and runtime checks.`;
787
+ }
788
+ return `Overall posture is ${score}/100. The areas needing attention are ${weak.join(', ')}; review the findings and fix the highest-severity items first.`;
789
+ }
790
+ function tierForScore(score) {
791
+ if (score >= 90)
792
+ return 'S';
793
+ if (score >= 70)
794
+ return 'A';
795
+ if (score >= 50)
796
+ return 'B';
797
+ return 'F';
798
+ }
799
+ function clampScore(score) {
800
+ return Math.max(0, Math.min(100, Math.round(score)));
801
+ }
802
+ function riskLevelToSeverity(risk) {
803
+ if (risk === 'critical')
804
+ return 'CRITICAL';
805
+ if (risk === 'high')
806
+ return 'HIGH';
807
+ if (risk === 'medium')
808
+ return 'MEDIUM';
809
+ return 'LOW';
810
+ }
421
811
  function buildSubscribeSummary(options) {
422
812
  const matched = options.results.reduce((acc, r) => acc + r.matchedArtifacts.length, 0);
423
- const shouldNotify = options.supported && matched > 0 && options.hardFailures === 0;
813
+ const shouldNotify = options.supported
814
+ && options.hardFailures === 0
815
+ && (options.quiet ? matched > 0 : options.fresh > 0);
424
816
  const summary = {
425
817
  supported: options.supported,
426
818
  pulled: options.pulled,
@@ -434,14 +826,31 @@ function buildSubscribeSummary(options) {
434
826
  installed: false,
435
827
  },
436
828
  };
437
- if (shouldNotify) {
829
+ if (shouldNotify && options.quiet) {
438
830
  summary.notification = {
439
831
  title: `AgentGuard detected ${matched} threat-feed match${matched === 1 ? '' : 'es'}`,
440
832
  body: formatThreatFeedNotification(options.results),
441
833
  };
442
834
  }
835
+ else if (shouldNotify) {
836
+ summary.notification = {
837
+ title: `AgentGuard found ${options.fresh} new threat-feed advisor${options.fresh === 1 ? 'y' : 'ies'}`,
838
+ body: formatNewAdvisoryNotification(options.freshAdvisories),
839
+ };
840
+ }
443
841
  return summary;
444
842
  }
843
+ function formatNewAdvisoryNotification(advisories) {
844
+ const lines = ['AgentGuard found new threat-feed advisories that need manual review:'];
845
+ for (const advisory of advisories.slice(0, 10)) {
846
+ lines.push(`- ${advisory.id} [${advisory.severity}] ${advisory.summary}`);
847
+ }
848
+ if (advisories.length > 10) {
849
+ lines.push(`- ... ${advisories.length - 10} more`);
850
+ }
851
+ lines.push('Run `agentguard subscribe --quiet` to execute the local self-check and report matches automatically.');
852
+ return lines.join('\n');
853
+ }
445
854
  function formatThreatFeedNotification(results) {
446
855
  const lines = ['AgentGuard threat-feed self-check found local matches:'];
447
856
  for (const result of results) {