@gendigital/sage-opencode 0.12.0 → 0.14.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.
Files changed (31) hide show
  1. package/dist/mcp-server.cjs +411 -88
  2. package/dist/mcp-server.cjs.map +4 -4
  3. package/dist/tool-handler.d.ts.map +1 -1
  4. package/dist/tool-handler.js +5 -3
  5. package/dist/tool-handler.js.map +1 -1
  6. package/package.json +3 -3
  7. package/resources/threats/_macros.yaml +193 -0
  8. package/resources/threats/agent-layer.yaml +19 -1
  9. package/resources/threats/commands.yaml +165 -31
  10. package/resources/threats/credentials.yaml +24 -4
  11. package/resources/threats/dummy.yaml +30 -0
  12. package/resources/threats/files.yaml +38 -0
  13. package/resources/threats/mac-commands.yaml +118 -27
  14. package/resources/threats/mac-credentials.yaml +34 -8
  15. package/resources/threats/mac-files.yaml +20 -0
  16. package/resources/threats/mac-obfuscation.yaml +28 -6
  17. package/resources/threats/mac-persistence.yaml +30 -4
  18. package/resources/threats/mac-supply-chain.yaml +12 -3
  19. package/resources/threats/mitre.yaml +325 -62
  20. package/resources/threats/obfuscation.yaml +33 -8
  21. package/resources/threats/persistence.yaml +40 -5
  22. package/resources/threats/prompt-injection.yaml +58 -0
  23. package/resources/threats/self-defense.yaml +15 -2
  24. package/resources/threats/supply_chain.yaml +6 -1
  25. package/resources/threats/urls.yaml +10 -0
  26. package/resources/threats/win-commands.yaml +294 -60
  27. package/resources/threats/win-credentials.yaml +68 -9
  28. package/resources/threats/win-files.yaml +20 -0
  29. package/resources/threats/win-obfuscation.yaml +56 -8
  30. package/resources/threats/win-persistence.yaml +45 -7
  31. package/resources/threats/win-supply-chain.yaml +5 -1
@@ -46,6 +46,39 @@ function getProcEnv() {
46
46
  function getHomeDir() {
47
47
  return getProcEnv().HOME || (0, import_node_os.homedir)();
48
48
  }
49
+ async function renameWithRetry(from, to, attempts = 5) {
50
+ if (process.platform !== "win32") {
51
+ await fsPromises.rename(from, to);
52
+ return;
53
+ }
54
+ for (let i = 0; i < attempts; i++) {
55
+ try {
56
+ await fsPromises.rename(from, to);
57
+ return;
58
+ } catch (err) {
59
+ const code = err.code;
60
+ const transient = code === "EPERM" || code === "EACCES" || code === "EBUSY";
61
+ if (!transient || i === attempts - 1)
62
+ throw err;
63
+ await new Promise((resolve2) => setTimeout(resolve2, 10 * 2 ** i));
64
+ }
65
+ }
66
+ }
67
+ async function atomicWriteJson(path2, data) {
68
+ await fsPromises.mkdir((0, import_node_path.dirname)(path2), { recursive: true });
69
+ const tmp = `${path2}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`;
70
+ try {
71
+ await fsPromises.writeFile(tmp, `${JSON.stringify(data, null, 2)}
72
+ `, { mode: 384 });
73
+ await renameWithRetry(tmp, path2);
74
+ } catch (err) {
75
+ try {
76
+ await fsPromises.unlink(tmp);
77
+ } catch {
78
+ }
79
+ throw err;
80
+ }
81
+ }
49
82
  async function removeStaleFileLock(lockPath, staleAgeMs) {
50
83
  try {
51
84
  const s = await fsPromises.stat(lockPath);
@@ -86,13 +119,15 @@ async function withFileLock(filePath, callback) {
86
119
  await release();
87
120
  }
88
121
  }
89
- var fs, fsPromises, import_node_os, import_promises, name1, name2;
122
+ var import_node_crypto, fs, fsPromises, import_node_os, import_node_path, import_promises, name1, name2;
90
123
  var init_file_utils = __esm({
91
124
  "../core/dist/file-utils.js"() {
92
125
  "use strict";
126
+ import_node_crypto = require("node:crypto");
93
127
  fs = __toESM(require("node:fs"), 1);
94
128
  fsPromises = __toESM(require("node:fs/promises"), 1);
95
129
  import_node_os = require("node:os");
130
+ import_node_path = require("node:path");
96
131
  import_promises = require("node:timers/promises");
97
132
  name1 = "read";
98
133
  name2 = "File";
@@ -4230,7 +4265,8 @@ var init_types2 = __esm({
4230
4265
  VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
4231
4266
  ThreatSchema = external_exports.object({
4232
4267
  id: external_exports.string(),
4233
- version: external_exports.number().int().optional(),
4268
+ version: external_exports.number().int().positive(),
4269
+ detectionName: external_exports.string(),
4234
4270
  category: external_exports.string(),
4235
4271
  severity: VerdictSeveritySchema,
4236
4272
  confidence: external_exports.number(),
@@ -4329,6 +4365,7 @@ var init_types2 = __esm({
4329
4365
  sensitivity: SensitivitySchema.default("balanced"),
4330
4366
  disabled_threats: external_exports.array(external_exports.string()).default([]),
4331
4367
  announce_clean_scans: external_exports.boolean().default(true),
4368
+ manage_status_line: external_exports.boolean().default(true),
4332
4369
  brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
4333
4370
  community_iq: external_exports.boolean().default(true)
4334
4371
  });
@@ -4344,38 +4381,48 @@ var init_types2 = __esm({
4344
4381
  });
4345
4382
 
4346
4383
  // ../core/dist/config.js
4384
+ function skillCacheTtlMs(config2) {
4385
+ return Math.max(1, config2.skill_check.cache_ttl_days) * MS_PER_DAY;
4386
+ }
4387
+ function isFreshTimestamp(ts, ttlMs, now = Date.now()) {
4388
+ if (Number.isNaN(ts))
4389
+ return false;
4390
+ if (ts - now > CLOCK_SKEW_TOLERANCE_MS)
4391
+ return false;
4392
+ return now - ts < ttlMs;
4393
+ }
4347
4394
  function resolvedSageDir() {
4348
4395
  return resolvePath(SAGE_DIR);
4349
4396
  }
4350
4397
  function defaultConfigPath() {
4351
- return (0, import_node_path.join)(resolvedSageDir(), "config.json");
4398
+ return (0, import_node_path2.join)(resolvedSageDir(), "config.json");
4352
4399
  }
4353
4400
  function defaultCachePath() {
4354
- return (0, import_node_path.join)(resolvedSageDir(), "cache.json");
4401
+ return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
4355
4402
  }
4356
4403
  function defaultExceptionsPath() {
4357
- return (0, import_node_path.join)(resolvedSageDir(), "exceptions.json");
4404
+ return (0, import_node_path2.join)(resolvedSageDir(), "exceptions.json");
4358
4405
  }
4359
4406
  function defaultAuditPath() {
4360
- return (0, import_node_path.join)(resolvedSageDir(), "audit.jsonl");
4407
+ return (0, import_node_path2.join)(resolvedSageDir(), "audit.jsonl");
4361
4408
  }
4362
4409
  function defaultOperationalLogPath() {
4363
- return (0, import_node_path.join)(resolvedSageDir(), "operational.jsonl");
4410
+ return (0, import_node_path2.join)(resolvedSageDir(), "operational.jsonl");
4364
4411
  }
4365
4412
  function resolvePath(pathStr) {
4366
4413
  if (pathStr.startsWith("~/") || pathStr === "~") {
4367
4414
  const home = getHomeDir();
4368
- return (0, import_node_path.join)(home, pathStr.slice(1));
4415
+ return (0, import_node_path2.join)(home, pathStr.slice(1));
4369
4416
  }
4370
4417
  return pathStr;
4371
4418
  }
4372
4419
  function isWithinDirectory(baseDir, targetPath) {
4373
- const rel = (0, import_node_path.relative)(baseDir, targetPath);
4420
+ const rel = (0, import_node_path2.relative)(baseDir, targetPath);
4374
4421
  if (rel === "")
4375
4422
  return true;
4376
- if ((0, import_node_path.isAbsolute)(rel))
4423
+ if ((0, import_node_path2.isAbsolute)(rel))
4377
4424
  return false;
4378
- return rel !== ".." && !rel.startsWith(`..${import_node_path.sep}`);
4425
+ return rel !== ".." && !rel.startsWith(`..${import_node_path2.sep}`);
4379
4426
  }
4380
4427
  function normalizeStateFilePath(configuredPath, fallbackPath, field, logger2) {
4381
4428
  const sageDir = resolvedSageDir();
@@ -4388,7 +4435,7 @@ function normalizeStateFilePath(configuredPath, fallbackPath, field, logger2) {
4388
4435
  return fallbackPath;
4389
4436
  }
4390
4437
  const expanded = resolvePath(trimmed);
4391
- const resolved = (0, import_node_path.isAbsolute)(expanded) ? (0, import_node_path.resolve)(expanded) : (0, import_node_path.resolve)(sageDir, expanded);
4438
+ const resolved = (0, import_node_path2.isAbsolute)(expanded) ? (0, import_node_path2.resolve)(expanded) : (0, import_node_path2.resolve)(sageDir, expanded);
4392
4439
  if (isWithinDirectory(sageDir, resolved)) {
4393
4440
  if (resolved === sageDir) {
4394
4441
  logger2.warn(`Config ${field}.path must point to a file; using default`, {
@@ -4487,11 +4534,11 @@ async function loadConfig(configPath, logger2 = nullLogger) {
4487
4534
  return defaultConfig(logger2);
4488
4535
  }
4489
4536
  }
4490
- var import_node_path, SAGE_DIR, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
4537
+ var import_node_path2, SAGE_DIR, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
4491
4538
  var init_config = __esm({
4492
4539
  "../core/dist/config.js"() {
4493
4540
  "use strict";
4494
- import_node_path = require("node:path");
4541
+ import_node_path2 = require("node:path");
4495
4542
  init_file_utils();
4496
4543
  init_types2();
4497
4544
  SAGE_DIR = "~/.sage";
@@ -20546,12 +20593,53 @@ function safeTruncate(value, maxLen) {
20546
20593
  return value.slice(0, cutIndex);
20547
20594
  }
20548
20595
 
20596
+ // ../core/dist/detection-names.js
20597
+ var REPORTING_ENGINE_SHORTHANDS = {
20598
+ package: "sgpk",
20599
+ amsi: "sgam",
20600
+ heuristics: "sghe",
20601
+ ml: "sgml",
20602
+ skill: "skan"
20603
+ };
20604
+ var REPORTING_DETECTION_SUFFIX = "sage";
20605
+ function formatReportingDetectionName(canonicalName, engine, optionalData = "") {
20606
+ const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
20607
+ return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
20608
+ }
20609
+ var SKILL_DETECTION_NAMES = {
20610
+ HIGH: "Other:SageSuspiciousSkill-A [Susp]",
20611
+ CRITICAL: "Other:SageMaliciousSkill-A [Trj]"
20612
+ };
20613
+ function skillDetectionName(verdict) {
20614
+ return SKILL_DETECTION_NAMES[verdict.toUpperCase()];
20615
+ }
20616
+ function isSkillId(value) {
20617
+ return /^[0-9a-f]{64}$/i.test(value);
20618
+ }
20619
+ function skillReportingDetectionName(verdict, skillId) {
20620
+ const canonicalName = skillDetectionName(verdict);
20621
+ return canonicalName && isSkillId(skillId) ? formatReportingDetectionName(canonicalName, "skill", skillId.toLowerCase()) : void 0;
20622
+ }
20623
+ function buildSkillCheckSignal(skillId, analyzerVerdict, jobId) {
20624
+ const normalizedSkillId = skillId.toLowerCase();
20625
+ const normalizedVerdict = analyzerVerdict.toUpperCase();
20626
+ const detectionName = skillReportingDetectionName(normalizedVerdict, normalizedSkillId);
20627
+ if (!detectionName)
20628
+ return void 0;
20629
+ return {
20630
+ detection_name: detectionName,
20631
+ skill_id: normalizedSkillId,
20632
+ ...jobId ? { job_id: jobId } : {},
20633
+ analyzer_verdict: normalizedVerdict
20634
+ };
20635
+ }
20636
+
20549
20637
  // ../core/dist/audit-log.js
20550
20638
  init_file_utils();
20551
20639
 
20552
20640
  // ../core/dist/jsonl-log-writer.js
20553
20641
  var import_promises2 = require("node:fs/promises");
20554
- var import_node_path2 = require("node:path");
20642
+ var import_node_path3 = require("node:path");
20555
20643
  init_config();
20556
20644
  init_file_utils();
20557
20645
  var writeQueues = /* @__PURE__ */ new Map();
@@ -20588,7 +20676,7 @@ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
20588
20676
  });
20589
20677
  }
20590
20678
  async function appendJsonlEntryNow(path2, config2, entry) {
20591
- await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(path2), { recursive: true });
20679
+ await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(path2), { recursive: true });
20592
20680
  await rotateIfNeeded(path2, config2.max_bytes, config2.max_files);
20593
20681
  await (0, import_promises2.appendFile)(path2, `${JSON.stringify(entry)}
20594
20682
  `);
@@ -20782,15 +20870,15 @@ init_types2();
20782
20870
  init_types2();
20783
20871
 
20784
20872
  // ../core/dist/version.js
20785
- var import_node_path3 = require("node:path");
20873
+ var import_node_path4 = require("node:path");
20786
20874
  var import_node_url = require("node:url");
20787
20875
  init_file_utils();
20788
20876
  var import_meta = {};
20789
20877
  function resolveVersion() {
20790
20878
  if (true)
20791
- return "0.12.0";
20879
+ return "0.14.0";
20792
20880
  try {
20793
- const pkgPath = (0, import_node_path3.join)((0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
20881
+ const pkgPath = (0, import_node_path4.join)((0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
20794
20882
  const pkg = JSON.parse(getFileContentSync(pkgPath));
20795
20883
  if (typeof pkg.version === "string")
20796
20884
  return pkg.version;
@@ -20911,7 +20999,7 @@ init_types2();
20911
20999
  // ../core/dist/extended-info.js
20912
21000
  var import_promises3 = require("node:fs/promises");
20913
21001
  var import_node_os2 = require("node:os");
20914
- var import_node_path4 = require("node:path");
21002
+ var import_node_path5 = require("node:path");
20915
21003
  init_file_utils();
20916
21004
  init_types2();
20917
21005
  var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
@@ -20977,8 +21065,8 @@ function sanitizeDocument(parsed, logger2) {
20977
21065
  return out;
20978
21066
  }
20979
21067
  async function loadExtendedInfo(sageDirPath, logger2 = nullLogger) {
20980
- const sageDir = sageDirPath ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".sage");
20981
- const filePath = (0, import_node_path4.join)(sageDir, EXTENDED_INFO_FILENAME);
21068
+ const sageDir = sageDirPath ?? (0, import_node_path5.join)((0, import_node_os2.homedir)(), ".sage");
21069
+ const filePath = (0, import_node_path5.join)(sageDir, EXTENDED_INFO_FILENAME);
20982
21070
  const cached2 = cache.get(filePath);
20983
21071
  if (cached2)
20984
21072
  return cached2.value;
@@ -21076,14 +21164,14 @@ function mergeExtendedInfo(envelope, extendedInfo) {
21076
21164
  }
21077
21165
 
21078
21166
  // ../core/dist/installation-id.js
21079
- var import_node_crypto = require("node:crypto");
21167
+ var import_node_crypto2 = require("node:crypto");
21080
21168
  var import_promises4 = require("node:fs/promises");
21081
- var import_node_path5 = require("node:path");
21169
+ var import_node_path6 = require("node:path");
21082
21170
  init_config();
21083
21171
  init_file_utils();
21084
21172
  async function getInstallationId(sageDirPath) {
21085
21173
  const sageDir = sageDirPath ?? resolvePath("~/.sage");
21086
- const idPath = (0, import_node_path5.join)(sageDir, "installation-id");
21174
+ const idPath = (0, import_node_path6.join)(sageDir, "installation-id");
21087
21175
  let fileExists = false;
21088
21176
  try {
21089
21177
  const existing = await getFileContent(idPath, "utf-8");
@@ -21094,7 +21182,7 @@ async function getInstallationId(sageDirPath) {
21094
21182
  } catch {
21095
21183
  }
21096
21184
  try {
21097
- const id = (0, import_node_crypto.randomUUID)();
21185
+ const id = (0, import_node_crypto2.randomUUID)();
21098
21186
  await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
21099
21187
  await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
21100
21188
  return id;
@@ -21309,11 +21397,11 @@ function createOperationalLogger(config2, runtime) {
21309
21397
  }
21310
21398
 
21311
21399
  // ../core/dist/plugin-scan-cache.js
21312
- var import_node_path6 = require("node:path");
21400
+ var import_node_path7 = require("node:path");
21313
21401
  init_config();
21314
21402
  init_file_utils();
21315
21403
  init_types2();
21316
- var DEFAULT_CACHE_PATH = (0, import_node_path6.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
21404
+ var DEFAULT_CACHE_PATH = (0, import_node_path7.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
21317
21405
 
21318
21406
  // ../core/dist/plugin-scanner.js
21319
21407
  init_config();
@@ -21329,9 +21417,119 @@ init_types2();
21329
21417
  var PENDING_TTL_MS = 60 * 60 * 1e3;
21330
21418
 
21331
21419
  // ../core/dist/skill-verdict-cache.js
21420
+ var import_node_path8 = require("node:path");
21332
21421
  init_config();
21333
21422
  init_file_utils();
21334
21423
  init_types2();
21424
+ var SCHEMA_VERSION = 1;
21425
+ var DEFAULT_VERDICT_TTL_MS = MS_PER_DAY;
21426
+ function parseSources(raw) {
21427
+ if (!Array.isArray(raw))
21428
+ return void 0;
21429
+ const sources = [];
21430
+ for (const item of raw) {
21431
+ if (!item || typeof item !== "object")
21432
+ continue;
21433
+ const entry = item;
21434
+ const source = {
21435
+ agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
21436
+ containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
21437
+ };
21438
+ if (source.agentRuntime || source.containerKey)
21439
+ sources.push(source);
21440
+ }
21441
+ return sources.length > 0 ? sources : void 0;
21442
+ }
21443
+ function defaultCachePath2() {
21444
+ return (0, import_node_path8.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
21445
+ }
21446
+ async function loadSkillVerdictCacheInternal(cachePath, ttlMs, logger2, pruneOnLoad) {
21447
+ let raw;
21448
+ try {
21449
+ raw = await getFileContent(cachePath);
21450
+ } catch {
21451
+ return { entries: {} };
21452
+ }
21453
+ try {
21454
+ const data = JSON.parse(raw);
21455
+ const rawEntries = data.entries ?? {};
21456
+ const now = Date.now();
21457
+ const entries = {};
21458
+ let pruned = 0;
21459
+ for (const [skillId, entry] of Object.entries(rawEntries)) {
21460
+ const analyzedAt = entry.analyzed_at;
21461
+ if (!analyzedAt)
21462
+ continue;
21463
+ const ts = Date.parse(analyzedAt);
21464
+ if (!isFreshTimestamp(ts, ttlMs, now)) {
21465
+ pruned++;
21466
+ continue;
21467
+ }
21468
+ entries[skillId] = {
21469
+ entryId: typeof entry.entry_id === "string" ? entry.entry_id : void 0,
21470
+ jobId: typeof entry.job_id === "string" ? entry.job_id : void 0,
21471
+ verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
21472
+ summary: typeof entry.summary === "string" ? entry.summary : void 0,
21473
+ skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
21474
+ sources: parseSources(entry.sources),
21475
+ analyzedAt
21476
+ };
21477
+ }
21478
+ const cache2 = { entries };
21479
+ if (pruneOnLoad && pruned > 0) {
21480
+ logger2.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
21481
+ await saveSkillVerdictCache(cache2, cachePath, logger2);
21482
+ }
21483
+ return cache2;
21484
+ } catch (e) {
21485
+ logger2.warn(`Failed to load verdict cache from ${cachePath}`, { error: String(e) });
21486
+ return { entries: {} };
21487
+ }
21488
+ }
21489
+ async function loadSkillVerdictCacheReadOnly(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger2 = nullLogger) {
21490
+ return loadSkillVerdictCacheInternal(cachePath, ttlMs, logger2, false);
21491
+ }
21492
+ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger2 = nullLogger) {
21493
+ try {
21494
+ const data = {
21495
+ schema_version: SCHEMA_VERSION,
21496
+ entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
21497
+ skillId,
21498
+ {
21499
+ entry_id: v.entryId,
21500
+ job_id: v.jobId,
21501
+ verdict: v.verdict,
21502
+ summary: v.summary,
21503
+ skill_name: v.skillName,
21504
+ sources: v.sources?.map((s) => ({
21505
+ agent_runtime: s.agentRuntime,
21506
+ container_key: s.containerKey
21507
+ })),
21508
+ analyzed_at: v.analyzedAt
21509
+ }
21510
+ ]))
21511
+ };
21512
+ await atomicWriteJson(cachePath, data);
21513
+ return true;
21514
+ } catch (e) {
21515
+ logger2.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
21516
+ return false;
21517
+ }
21518
+ }
21519
+
21520
+ // ../core/dist/skill-verdict-record.js
21521
+ var import_node_crypto3 = require("node:crypto");
21522
+ var SKILL_ENTRY_ID_NAMESPACE = Buffer.from("75bba79a376142a7bacbf95d791126d3", "hex");
21523
+ function legacySkillEntryId(skillId, analyzedAt) {
21524
+ const bytes = (0, import_node_crypto3.createHash)("sha1").update(SKILL_ENTRY_ID_NAMESPACE).update(`${skillId.toLowerCase()}:${analyzedAt}`).digest().subarray(0, 16);
21525
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
21526
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
21527
+ const hex = bytes.toString("hex");
21528
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
21529
+ }
21530
+ function resolveSkillVerdictEntryId(skillId, entry) {
21531
+ return entry.entryId ?? legacySkillEntryId(skillId, entry.analyzedAt);
21532
+ }
21335
21533
 
21336
21534
  // ../core/dist/plugin-scanner.js
21337
21535
  init_types2();
@@ -21339,12 +21537,12 @@ var MAX_FILE_SIZE = 512 * 1024;
21339
21537
 
21340
21538
  // ../core/dist/product-version.js
21341
21539
  var import_node_fs = require("node:fs");
21342
- var import_node_path7 = __toESM(require("node:path"), 1);
21540
+ var import_node_path9 = __toESM(require("node:path"), 1);
21343
21541
  function readProductJsonVersion(appRoot) {
21344
21542
  if (!appRoot)
21345
21543
  return "unknown";
21346
21544
  try {
21347
- const productJsonPath = import_node_path7.default.join(appRoot, "product.json");
21545
+ const productJsonPath = import_node_path9.default.join(appRoot, "product.json");
21348
21546
  const raw = (0, import_node_fs.readFileSync)(productJsonPath, "utf8");
21349
21547
  const parsed = JSON.parse(raw);
21350
21548
  const version2 = parsed.version;
@@ -31407,7 +31605,7 @@ var StdioServerTransport = class {
31407
31605
  };
31408
31606
 
31409
31607
  // ../mcp/dist/tools/false-positive.js
31410
- var import_node_crypto2 = require("node:crypto");
31608
+ var import_node_crypto4 = require("node:crypto");
31411
31609
  init_zod();
31412
31610
 
31413
31611
  // ../mcp/dist/tools/utils.js
@@ -31437,17 +31635,79 @@ function readContent(entry) {
31437
31635
  return {};
31438
31636
  return raw;
31439
31637
  }
31638
+ function readContentSnippet(entry) {
31639
+ return asString(entry.content_snippet);
31640
+ }
31440
31641
  function isRuntimeVerdictEntry(v) {
31441
31642
  if (!v || typeof v !== "object" || Array.isArray(v))
31442
31643
  return false;
31443
31644
  return v.type === "runtime_verdict";
31444
31645
  }
31646
+ function isAnalyzedSkillVerdictEntry(v) {
31647
+ if (!v || typeof v !== "object" || Array.isArray(v))
31648
+ return false;
31649
+ const entry = v;
31650
+ return entry.type === "skill_verdict" && entry.status === "analyzed" && isSkillId(asString(entry.skill_id) ?? "");
31651
+ }
31652
+ function isReportableSkillVerdictEntry(v) {
31653
+ if (!isAnalyzedSkillVerdictEntry(v))
31654
+ return false;
31655
+ const entry = v;
31656
+ const verdict = asString(entry.verdict)?.toUpperCase();
31657
+ return (verdict === "HIGH" || verdict === "CRITICAL") && asString(entry.entry_id) !== void 0;
31658
+ }
31659
+ function latestReportableSkillVerdicts(entries, ttlMs, now = Date.now()) {
31660
+ const latestBySkillId = /* @__PURE__ */ new Map();
31661
+ for (const entry of entries) {
31662
+ if (!isAnalyzedSkillVerdictEntry(entry))
31663
+ continue;
31664
+ const timestamp = Date.parse(asString(entry.timestamp) ?? "");
31665
+ if (!isFreshTimestamp(timestamp, ttlMs, now))
31666
+ continue;
31667
+ const normalizedSkillId = entry.skill_id.toLowerCase();
31668
+ const current = latestBySkillId.get(normalizedSkillId);
31669
+ if (current && timestamp < current.timestamp)
31670
+ continue;
31671
+ latestBySkillId.delete(normalizedSkillId);
31672
+ latestBySkillId.set(normalizedSkillId, { entry, timestamp });
31673
+ }
31674
+ return [...latestBySkillId.values()].map(({ entry }) => entry).filter(isReportableSkillVerdictEntry);
31675
+ }
31676
+ async function cachedSkillVerdictEntries(config2, logger2) {
31677
+ const cache2 = await loadSkillVerdictCacheReadOnly(void 0, skillCacheTtlMs(config2), logger2);
31678
+ return Object.entries(cache2.entries).flatMap(([skillId, entry]) => {
31679
+ if (!isSkillId(skillId))
31680
+ return [];
31681
+ if (!entry.verdict)
31682
+ return [];
31683
+ const normalizedSkillId = skillId.toLowerCase();
31684
+ const analyzerVerdict = entry.verdict.toUpperCase();
31685
+ const signal = buildSkillCheckSignal(normalizedSkillId, analyzerVerdict, entry.jobId);
31686
+ return [
31687
+ {
31688
+ type: "skill_verdict",
31689
+ entry_id: resolveSkillVerdictEntryId(normalizedSkillId, entry),
31690
+ timestamp: entry.analyzedAt,
31691
+ agent_runtime: entry.sources?.[0]?.agentRuntime,
31692
+ skill_id: normalizedSkillId,
31693
+ job_id: entry.jobId,
31694
+ status: "analyzed",
31695
+ verdict: analyzerVerdict,
31696
+ signals: signal ? { skill_checks: [signal] } : void 0
31697
+ }
31698
+ ];
31699
+ });
31700
+ }
31445
31701
  function getConversationId(entry) {
31446
31702
  return asString(entry.conversation_id) ?? asString(entry.session_id);
31447
31703
  }
31448
31704
  function getEntryId(entry) {
31449
31705
  return asString(entry.entry_id);
31450
31706
  }
31707
+ function getEntryTimestamp(entry) {
31708
+ const timestamp = Date.parse(asString(entry.timestamp) ?? "");
31709
+ return Number.isFinite(timestamp) ? timestamp : 0;
31710
+ }
31451
31711
  function asHookType(value) {
31452
31712
  const s = asString(value);
31453
31713
  if (s === "PreToolUse" || s === "PostToolUse" || s === "SessionStart" || s === "GatewayStart" || s === "BeforeAgentStart" || s === "MessagesTransform") {
@@ -31468,9 +31728,10 @@ async function postMultipart(endpoint, payload, timeoutSeconds) {
31468
31728
  const bodyText = await res.text().catch(() => "");
31469
31729
  return { ok: res.ok, status: res.status, bodyText };
31470
31730
  }
31731
+ var MAX_AUDIT_SCAN_ENTRIES = 5e3;
31471
31732
  var ListInputSchema = external_exports.object({
31472
31733
  conversation_id: external_exports.string().optional().describe("Conversation id to filter audit entries; if omitted, uses the most recent one."),
31473
- limit: external_exports.number().int().min(1).max(5e3).optional().describe("Max entries to scan and return.")
31734
+ limit: external_exports.number().int().min(1).max(MAX_AUDIT_SCAN_ENTRIES).optional().describe("Max merged entries to return.")
31474
31735
  });
31475
31736
  var MAX_IMPLICIT_ENTRIES = 3;
31476
31737
  var MAX_EXPLICIT_ENTRIES = 10;
@@ -31479,12 +31740,13 @@ var ReportInputSchema = external_exports.object({
31479
31740
  description: external_exports.string().min(1).describe("Short description of what is a false positive."),
31480
31741
  reasoning: external_exports.string().min(1).describe("Reasoning for why this should be treated as a false positive."),
31481
31742
  agent_runtime_version: external_exports.string().optional().describe("Optional agent runtime version override (Cursor/VS Code/Claude version). If omitted, the server resolves the host version from SAGE_APP_ROOT (the application root injected by the Sage extension at MCP-server install time, used to read product.json), then falls back to SAGE_AGENT_RUNTIME_VERSION env, then 'unknown'."),
31482
- entry_ids: external_exports.array(external_exports.string()).optional().describe(`REQUIRED in normal use: list of specific audit entry_ids to report as false positives. ALWAYS call sage_list_audit_entries first to inspect the audit log and identify the exact deny/ask verdict the user considers a false positive, then pass its entry_id here. Up to ${MAX_EXPLICIT_ENTRIES} entry_ids may be provided per call. Omitting this parameter is a fallback that submits up to ${MAX_IMPLICIT_ENTRIES} most recent deny/ask entries for the conversation, which may include irrelevant verdicts and should only be used when entry_id discovery is impossible.`),
31743
+ entry_ids: external_exports.array(external_exports.string()).max(MAX_EXPLICIT_ENTRIES).optional().describe(`REQUIRED in normal use: list of specific audit entry_ids to report as false positives. ALWAYS call sage_list_audit_entries first to inspect the audit log and identify the exact deny/ask verdict the user considers a false positive, then pass its entry_id here. Up to ${MAX_EXPLICIT_ENTRIES} entry_ids may be provided per call. Omitting this parameter is a fallback that submits up to ${MAX_IMPLICIT_ENTRIES} most recent deny/ask entries for the conversation, which may include irrelevant verdicts and should only be used when entry_id discovery is impossible.`),
31483
31744
  dry_run: external_exports.boolean().optional().describe("If true, do not POST; just show the payload.")
31484
31745
  });
31485
- function parseAuditSignals(raw) {
31746
+ function parseRuntimeAuditSignals(raw, options = {}) {
31486
31747
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
31487
31748
  return {};
31749
+ const includePiContentSnippet = options.includePiContentSnippet ?? true;
31488
31750
  const obj = raw;
31489
31751
  const heuristicsRaw = obj.heuristics;
31490
31752
  const urlChecksRaw = obj.url_checks;
@@ -31496,11 +31758,16 @@ function parseAuditSignals(raw) {
31496
31758
  if (!h || typeof h !== "object" || Array.isArray(h))
31497
31759
  return null;
31498
31760
  const rec = h;
31761
+ const detection_name = asString(rec.detection_name);
31499
31762
  const rule_id = asString(rec.rule_id);
31500
31763
  const rule_version = typeof rec.rule_version === "number" ? Math.trunc(rec.rule_version) : void 0;
31501
31764
  if (!rule_id)
31502
31765
  return null;
31503
- return { rule_id, rule_version };
31766
+ return {
31767
+ ...detection_name ? { detection_name } : {},
31768
+ rule_id,
31769
+ rule_version
31770
+ };
31504
31771
  }).filter(Boolean) : void 0;
31505
31772
  const url_checks = Array.isArray(urlChecksRaw) ? urlChecksRaw.map((u) => {
31506
31773
  if (!u || typeof u !== "object" || Array.isArray(u))
@@ -31538,12 +31805,20 @@ function parseAuditSignals(raw) {
31538
31805
  if (!m || typeof m !== "object" || Array.isArray(m))
31539
31806
  return null;
31540
31807
  const rec = m;
31808
+ const detection_name = asString(rec.detection_name);
31541
31809
  const risk = typeof rec.risk === "number" ? rec.risk : void 0;
31542
31810
  const model_id = asString(rec.model_id);
31543
31811
  const content_name = asString(rec.content_name);
31812
+ const content_snippet = asString(rec.content_snippet);
31544
31813
  if (risk === void 0 || !model_id || !content_name)
31545
31814
  return null;
31546
- return { risk, model_id, content_name };
31815
+ return {
31816
+ ...detection_name ? { detection_name } : {},
31817
+ risk,
31818
+ model_id,
31819
+ content_name,
31820
+ ...content_snippet && includePiContentSnippet ? { content_snippet } : {}
31821
+ };
31547
31822
  }).filter(Boolean) : void 0;
31548
31823
  const amsi_checks = Array.isArray(amsiChecksRaw) ? amsiChecksRaw.map((a) => {
31549
31824
  if (!a || typeof a !== "object" || Array.isArray(a))
@@ -31577,7 +31852,7 @@ function registerFalsePositiveTools(server, opts) {
31577
31852
  const branding = opts.branding ?? defaultBranding;
31578
31853
  server.registerTool("sage_list_audit_entries", {
31579
31854
  title: `${branding.name}: List Audit Entries`,
31580
- description: `List recent ${branding.name} audit log entries, optionally scoped to a conversation id. Useful for selecting entry_ids to report as false positives.`,
31855
+ description: `List recent ${branding.name} runtime and Skill Analyzer audit entries. Runtime entries are optionally scoped to a conversation id; skill entries are global because analysis can finish outside the originating conversation. Useful for selecting entry_ids to report as false positives.`,
31581
31856
  inputSchema: ListInputSchema
31582
31857
  }, async ({ conversation_id, limit }) => {
31583
31858
  const complete = (result, data = {}) => {
@@ -31595,29 +31870,59 @@ function registerFalsePositiveTools(server, opts) {
31595
31870
  });
31596
31871
  try {
31597
31872
  const config2 = await loadConfig(void 0, logger2);
31598
- const scanLimit = limit ?? 500;
31599
- const entries = await getRecentEntries(config2.logging, scanLimit);
31873
+ const outputLimit = limit ?? 500;
31874
+ const entries = await getRecentEntries(config2.logging, MAX_AUDIT_SCAN_ENTRIES);
31600
31875
  const runtime = entries.filter(isRuntimeVerdictEntry);
31876
+ const cachedSkills = await cachedSkillVerdictEntries(config2, logger2);
31877
+ const skills = latestReportableSkillVerdicts([...entries, ...cachedSkills], skillCacheTtlMs(config2));
31601
31878
  const inferredConversationId = conversation_id ?? getConversationId(runtime.slice().reverse().find((e) => !!getConversationId(e)) ?? {});
31602
- if (!inferredConversationId) {
31603
- complete("skipped", { skippedReason: "no_runtime_entries", entriesReturned: 0 });
31604
- return textResult("No runtime_verdict entries found in the audit log.", true);
31605
- }
31606
- const filtered = runtime.filter((e) => getConversationId(e) === inferredConversationId);
31607
- const out = filtered.map((e) => ({
31608
- entry_id: getEntryId(e),
31609
- timestamp: asString(e.timestamp),
31610
- conversation_id: inferredConversationId,
31611
- agent_runtime: asString(e.agent_runtime),
31612
- tool_name: asString(e.tool_name),
31613
- tool_input_summary: asString(e.tool_input_summary),
31614
- verdict: asString(e.verdict),
31615
- severity: asString(e.severity),
31616
- source: asString(e.source),
31617
- user_override: e.user_override,
31618
- signals: parseAuditSignals(e.signals),
31619
- content: readContent(e)
31620
- }));
31879
+ if (!inferredConversationId && skills.length === 0) {
31880
+ complete("skipped", { skippedReason: "no_reportable_entries", entriesReturned: 0 });
31881
+ return textResult("No reportable runtime or skill verdict entries found.", true);
31882
+ }
31883
+ const filteredRuntime = inferredConversationId ? runtime.filter((e) => getConversationId(e) === inferredConversationId) : [];
31884
+ const selected = [
31885
+ ...filteredRuntime,
31886
+ ...skills
31887
+ ].sort((a, b) => getEntryTimestamp(a) - getEntryTimestamp(b)).slice(-outputLimit);
31888
+ const out = selected.map((entry) => {
31889
+ if (isReportableSkillVerdictEntry(entry)) {
31890
+ const skillId = entry.skill_id;
31891
+ const analyzerVerdict = asString(entry.verdict)?.toUpperCase() ?? "";
31892
+ const signal = buildSkillCheckSignal(skillId, analyzerVerdict, asString(entry.job_id));
31893
+ return {
31894
+ type: "skill_verdict",
31895
+ entry_id: getEntryId(entry),
31896
+ timestamp: asString(entry.timestamp),
31897
+ agent_runtime: asString(entry.agent_runtime),
31898
+ skill_id: skillId.toLowerCase(),
31899
+ job_id: asString(entry.job_id),
31900
+ status: asString(entry.status),
31901
+ analyzer_verdict: analyzerVerdict,
31902
+ signals: signal ? { skill_checks: [signal] } : {}
31903
+ };
31904
+ }
31905
+ const runtimeEntry = entry;
31906
+ return {
31907
+ type: "runtime_verdict",
31908
+ entry_id: getEntryId(runtimeEntry),
31909
+ timestamp: asString(runtimeEntry.timestamp),
31910
+ conversation_id: inferredConversationId,
31911
+ agent_runtime: asString(runtimeEntry.agent_runtime),
31912
+ tool_name: asString(runtimeEntry.tool_name),
31913
+ tool_input_summary: asString(runtimeEntry.tool_input_summary),
31914
+ verdict: asString(runtimeEntry.verdict),
31915
+ severity: asString(runtimeEntry.severity),
31916
+ source: asString(runtimeEntry.source),
31917
+ user_override: runtimeEntry.user_override,
31918
+ // PI snippets are untrusted fetched content. Exclude their raw
31919
+ // text from this model-visible response.
31920
+ signals: parseRuntimeAuditSignals(runtimeEntry.signals, {
31921
+ includePiContentSnippet: false
31922
+ }),
31923
+ content: readContent(runtimeEntry)
31924
+ };
31925
+ });
31621
31926
  complete("completed", {
31622
31927
  conversationId: inferredConversationId,
31623
31928
  entriesReturned: out.length
@@ -31634,11 +31939,11 @@ function registerFalsePositiveTools(server, opts) {
31634
31939
  });
31635
31940
  server.registerTool("sage_report_false_positive", {
31636
31941
  title: `${branding.name}: Report False Positive`,
31637
- description: `Report ${branding.name} audit log entries as false positives to the backend, scoped to the current conversation.
31942
+ description: `Report ${branding.name} runtime or Skill Analyzer audit entries as false positives to the backend. Runtime entries are scoped to the current conversation; skill entries require explicit entry_ids.
31638
31943
 
31639
31944
  USAGE \u2014 IMPORTANT:
31640
- 1. ALWAYS call \`sage_list_audit_entries\` FIRST to inspect the audit log and locate the specific deny/ask verdict the user considers a false positive.
31641
- 2. ALWAYS pass the \`entry_ids\` parameter with just the relevant deny/ask entry id(s). Up to ${MAX_EXPLICIT_ENTRIES} entry_ids may be reported per call.
31945
+ 1. ALWAYS call \`sage_list_audit_entries\` FIRST to inspect the audit log and locate the specific runtime or skill verdict the user considers a false positive.
31946
+ 2. ALWAYS pass the \`entry_ids\` parameter with just the relevant entry id(s); it is required for skill verdicts. Up to ${MAX_EXPLICIT_ENTRIES} entry_ids may be reported per call.
31642
31947
  3. Only when entry_id discovery is impossible should \`entry_ids\` be omitted; in that fallback at most ${MAX_IMPLICIT_ENTRIES} most recent deny/ask entries for the conversation are submitted, which may include irrelevant verdicts and floods the backend with noise.`,
31643
31948
  inputSchema: ReportInputSchema
31644
31949
  }, async ({ conversation_id, description, reasoning, agent_runtime_version, entry_ids, dry_run }) => {
@@ -31662,31 +31967,35 @@ USAGE \u2014 IMPORTANT:
31662
31967
  const config2 = await loadConfig(void 0, logger2);
31663
31968
  const endpoint = resolveEndpoint("/v2/fp-report");
31664
31969
  const timeoutSeconds = Number(process.env.SAGE_FALSE_POSITIVE_TIMEOUT_SECONDS) || 5;
31665
- const scanLimit = 2e3;
31666
- const entries = await getRecentEntries(config2.logging, scanLimit);
31970
+ const entries = await getRecentEntries(config2.logging, MAX_AUDIT_SCAN_ENTRIES);
31667
31971
  const runtime = entries.filter(isRuntimeVerdictEntry);
31972
+ const cachedSkills = await cachedSkillVerdictEntries(config2, logger2);
31973
+ const skills = latestReportableSkillVerdicts([...entries, ...cachedSkills], skillCacheTtlMs(config2));
31668
31974
  const inferredConversationId = conversation_id ?? getConversationId(runtime.slice().reverse().find((e) => !!getConversationId(e)) ?? {});
31669
- if (!inferredConversationId) {
31670
- complete("skipped", { skippedReason: "no_runtime_entries", reportsCount: 0 });
31671
- return textResult("No runtime_verdict entries found in the audit log.", true);
31672
- }
31673
- let filtered = runtime.filter((e) => getConversationId(e) === inferredConversationId);
31975
+ let filtered;
31674
31976
  if (entry_ids && entry_ids.length > 0) {
31675
- const wanted = new Set(entry_ids);
31676
- filtered = filtered.filter((e) => {
31677
- const id = getEntryId(e);
31678
- return id ? wanted.has(id) : false;
31679
- });
31680
- if (filtered.length > MAX_EXPLICIT_ENTRIES) {
31977
+ if (entry_ids.length > MAX_EXPLICIT_ENTRIES) {
31681
31978
  complete("failed_validation", {
31682
31979
  conversationId: inferredConversationId,
31683
- reportsCount: filtered.length,
31980
+ entryIdsCount: entry_ids.length,
31684
31981
  maxEntries: MAX_EXPLICIT_ENTRIES
31685
31982
  });
31686
- return textResult(`Too many entry_ids (${filtered.length}). Provide at most ${MAX_EXPLICIT_ENTRIES} specific entry_ids per report call.`, true);
31983
+ return textResult(`Too many entry_ids (${entry_ids.length}). Provide at most ${MAX_EXPLICIT_ENTRIES} specific entry_ids per report call.`, true);
31687
31984
  }
31985
+ const wanted = new Set(entry_ids);
31986
+ const scopedRuntime = inferredConversationId ? runtime.filter((e) => getConversationId(e) === inferredConversationId) : [];
31987
+ filtered = [...scopedRuntime, ...skills].filter((e) => {
31988
+ const id = getEntryId(e);
31989
+ return id !== void 0 && wanted.has(id);
31990
+ });
31688
31991
  } else {
31689
- filtered = filtered.filter((e) => {
31992
+ if (!inferredConversationId) {
31993
+ complete("skipped", { skippedReason: "no_runtime_entries", reportsCount: 0 });
31994
+ return textResult("No runtime_verdict entries found. Skill verdicts must be reported with explicit entry_ids.", true);
31995
+ }
31996
+ filtered = runtime.filter((e) => {
31997
+ if (getConversationId(e) !== inferredConversationId)
31998
+ return false;
31690
31999
  const v = asString(e.verdict);
31691
32000
  return v === "deny" || v === "ask";
31692
32001
  });
@@ -31701,7 +32010,7 @@ USAGE \u2014 IMPORTANT:
31701
32010
  skippedReason: "no_matching_entry_ids",
31702
32011
  reportsCount: 0
31703
32012
  });
31704
- return textResult("No matching audit entries found for the provided entry_ids in this conversation. Run sage_list_audit_entries first.", true);
32013
+ return textResult("No matching reportable audit entries found for the provided entry_ids. Run sage_list_audit_entries first.", true);
31705
32014
  }
31706
32015
  complete("skipped", {
31707
32016
  conversationId: inferredConversationId,
@@ -31727,15 +32036,22 @@ USAGE \u2014 IMPORTANT:
31727
32036
  ${reasoning}`;
31728
32037
  const extendedInfo = await loadExtendedInfo(void 0, logger2).catch(() => null);
31729
32038
  const reports = filtered.map((e) => {
32039
+ const isSkill = isReportableSkillVerdictEntry(e);
32040
+ const runtimeEntry = isSkill ? void 0 : e;
31730
32041
  const entry_id = getEntryId(e);
31731
32042
  const timestamp = asString(e.timestamp) ?? (/* @__PURE__ */ new Date()).toISOString();
31732
32043
  const agent_runtime = asString(e.agent_runtime) ?? "unknown";
31733
- const tool_type = asString(e.tool_name) ?? "Unknown";
31734
- const verdict = asString(e.verdict) ?? "deny";
31735
- const user_action = e.user_override === true ? "allowed" : "blocked";
31736
- const hook_type = asHookType(e.hook_type) ?? "PreToolUse";
31737
- const signals = parseAuditSignals(e.signals);
31738
- const content = readContent(e);
32044
+ const analyzerVerdict = isSkill ? asString(e.verdict)?.toUpperCase() : void 0;
32045
+ const tool_type = isSkill ? "Skill" : asString(runtimeEntry?.tool_name) ?? "Unknown";
32046
+ const verdict = isSkill ? analyzerVerdict === "CRITICAL" ? "deny" : "ask" : asString(e.verdict) ?? "deny";
32047
+ const wasAllowed = !isSkill && (verdict === "allow" || runtimeEntry?.user_override === true);
32048
+ const user_action = wasAllowed ? "allowed" : "blocked";
32049
+ const hook_type = isSkill ? "SessionStart" : asHookType(runtimeEntry?.hook_type) ?? "PreToolUse";
32050
+ const signals = parseRuntimeAuditSignals(e.signals, {
32051
+ includePiContentSnippet: !dry_run
32052
+ });
32053
+ const content = runtimeEntry ? readContent(runtimeEntry) : {};
32054
+ const contentSnippet = runtimeEntry ? readContentSnippet(runtimeEntry) : void 0;
31739
32055
  const bestEffortSignals = {};
31740
32056
  if (signals.heuristics)
31741
32057
  bestEffortSignals.heuristics = signals.heuristics;
@@ -31749,6 +32065,11 @@ ${reasoning}`;
31749
32065
  bestEffortSignals.pi_checks = signals.pi_checks;
31750
32066
  if (signals.amsi_checks)
31751
32067
  bestEffortSignals.amsi_checks = signals.amsi_checks;
32068
+ if (isSkill) {
32069
+ const signal = analyzerVerdict ? buildSkillCheckSignal(e.skill_id, analyzerVerdict, asString(e.job_id)) : void 0;
32070
+ if (signal)
32071
+ bestEffortSignals.skill_checks = [signal];
32072
+ }
31752
32073
  const baseReportPayload = {
31753
32074
  ...buildSageProxyEnvelope({
31754
32075
  iid,
@@ -31764,10 +32085,11 @@ ${reasoning}`;
31764
32085
  user_action,
31765
32086
  timestamp,
31766
32087
  ...Object.keys(bestEffortSignals).length > 0 ? { signals: bestEffortSignals } : {},
31767
- content
32088
+ ...Object.keys(content).length > 0 ? { content } : {},
32089
+ ...contentSnippet && !dry_run ? { content_snippet: contentSnippet } : {}
31768
32090
  },
31769
32091
  comment,
31770
- event_id: entry_id ?? (0, import_node_crypto2.randomUUID)()
32092
+ event_id: entry_id ?? (0, import_node_crypto4.randomUUID)()
31771
32093
  };
31772
32094
  const reportPayload = mergeExtendedInfo(baseReportPayload, extendedInfo);
31773
32095
  return { entry_id, payload: reportPayload };
@@ -31821,7 +32143,8 @@ ${JSON.stringify(summary, null, 2)}`, true);
31821
32143
  conversationId: inferredConversationId,
31822
32144
  reported: filtered.length
31823
32145
  });
31824
- return textResult(`Reported ${filtered.length} audit entr${filtered.length === 1 ? "y" : "ies"} for conversation_id=${inferredConversationId}.`);
32146
+ const scope = inferredConversationId ? ` for conversation_id=${inferredConversationId}` : "";
32147
+ return textResult(`Reported ${filtered.length} audit entr${filtered.length === 1 ? "y" : "ies"}${scope}.`);
31825
32148
  } catch (e) {
31826
32149
  logger2.warn("MCP tool failed", {
31827
32150
  toolName: "sage_report_false_positive",
@@ -31869,7 +32192,7 @@ async function main() {
31869
32192
  );
31870
32193
  const branding = resolveBranding(config2.brand_key, logger);
31871
32194
  await runSageMcpServerStdio({
31872
- version: "0.12.0",
32195
+ version: "0.14.0",
31873
32196
  logger,
31874
32197
  branding
31875
32198
  });