@gendigital/sage-opencode 0.13.0 → 0.15.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.
@@ -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";
@@ -4206,7 +4241,7 @@ var init_zod = __esm({
4206
4241
  });
4207
4242
 
4208
4243
  // ../core/dist/types.js
4209
- var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, SkillCheckConfigSchema, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4244
+ var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, SkillCheckConfigSchema, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4210
4245
  var init_types2 = __esm({
4211
4246
  "../core/dist/types.js"() {
4212
4247
  "use strict";
@@ -4295,6 +4330,7 @@ var init_types2 = __esm({
4295
4330
  */
4296
4331
  upload_enabled: external_exports.boolean().default(true)
4297
4332
  });
4333
+ DEFAULT_PI_MEDIUM_RISK_THRESHOLD = 0.5;
4298
4334
  PiCheckConfigSchema = external_exports.object({
4299
4335
  enabled: external_exports.boolean().default(false),
4300
4336
  max_content_length: external_exports.number().default(16384),
@@ -4346,38 +4382,48 @@ var init_types2 = __esm({
4346
4382
  });
4347
4383
 
4348
4384
  // ../core/dist/config.js
4385
+ function skillCacheTtlMs(config2) {
4386
+ return Math.max(1, config2.skill_check.cache_ttl_days) * MS_PER_DAY;
4387
+ }
4388
+ function isFreshTimestamp(ts, ttlMs, now = Date.now()) {
4389
+ if (Number.isNaN(ts))
4390
+ return false;
4391
+ if (ts - now > CLOCK_SKEW_TOLERANCE_MS)
4392
+ return false;
4393
+ return now - ts < ttlMs;
4394
+ }
4349
4395
  function resolvedSageDir() {
4350
4396
  return resolvePath(SAGE_DIR);
4351
4397
  }
4352
4398
  function defaultConfigPath() {
4353
- return (0, import_node_path.join)(resolvedSageDir(), "config.json");
4399
+ return (0, import_node_path2.join)(resolvedSageDir(), "config.json");
4354
4400
  }
4355
4401
  function defaultCachePath() {
4356
- return (0, import_node_path.join)(resolvedSageDir(), "cache.json");
4402
+ return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
4357
4403
  }
4358
4404
  function defaultExceptionsPath() {
4359
- return (0, import_node_path.join)(resolvedSageDir(), "exceptions.json");
4405
+ return (0, import_node_path2.join)(resolvedSageDir(), "exceptions.json");
4360
4406
  }
4361
4407
  function defaultAuditPath() {
4362
- return (0, import_node_path.join)(resolvedSageDir(), "audit.jsonl");
4408
+ return (0, import_node_path2.join)(resolvedSageDir(), "audit.jsonl");
4363
4409
  }
4364
4410
  function defaultOperationalLogPath() {
4365
- return (0, import_node_path.join)(resolvedSageDir(), "operational.jsonl");
4411
+ return (0, import_node_path2.join)(resolvedSageDir(), "operational.jsonl");
4366
4412
  }
4367
4413
  function resolvePath(pathStr) {
4368
4414
  if (pathStr.startsWith("~/") || pathStr === "~") {
4369
4415
  const home = getHomeDir();
4370
- return (0, import_node_path.join)(home, pathStr.slice(1));
4416
+ return (0, import_node_path2.join)(home, pathStr.slice(1));
4371
4417
  }
4372
4418
  return pathStr;
4373
4419
  }
4374
4420
  function isWithinDirectory(baseDir, targetPath) {
4375
- const rel = (0, import_node_path.relative)(baseDir, targetPath);
4421
+ const rel = (0, import_node_path2.relative)(baseDir, targetPath);
4376
4422
  if (rel === "")
4377
4423
  return true;
4378
- if ((0, import_node_path.isAbsolute)(rel))
4424
+ if ((0, import_node_path2.isAbsolute)(rel))
4379
4425
  return false;
4380
- return rel !== ".." && !rel.startsWith(`..${import_node_path.sep}`);
4426
+ return rel !== ".." && !rel.startsWith(`..${import_node_path2.sep}`);
4381
4427
  }
4382
4428
  function normalizeStateFilePath(configuredPath, fallbackPath, field, logger2) {
4383
4429
  const sageDir = resolvedSageDir();
@@ -4390,7 +4436,7 @@ function normalizeStateFilePath(configuredPath, fallbackPath, field, logger2) {
4390
4436
  return fallbackPath;
4391
4437
  }
4392
4438
  const expanded = resolvePath(trimmed);
4393
- const resolved = (0, import_node_path.isAbsolute)(expanded) ? (0, import_node_path.resolve)(expanded) : (0, import_node_path.resolve)(sageDir, expanded);
4439
+ const resolved = (0, import_node_path2.isAbsolute)(expanded) ? (0, import_node_path2.resolve)(expanded) : (0, import_node_path2.resolve)(sageDir, expanded);
4394
4440
  if (isWithinDirectory(sageDir, resolved)) {
4395
4441
  if (resolved === sageDir) {
4396
4442
  logger2.warn(`Config ${field}.path must point to a file; using default`, {
@@ -4489,11 +4535,11 @@ async function loadConfig(configPath, logger2 = nullLogger) {
4489
4535
  return defaultConfig(logger2);
4490
4536
  }
4491
4537
  }
4492
- var import_node_path, SAGE_DIR, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
4538
+ var import_node_path2, SAGE_DIR, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
4493
4539
  var init_config = __esm({
4494
4540
  "../core/dist/config.js"() {
4495
4541
  "use strict";
4496
- import_node_path = require("node:path");
4542
+ import_node_path2 = require("node:path");
4497
4543
  init_file_utils();
4498
4544
  init_types2();
4499
4545
  SAGE_DIR = "~/.sage";
@@ -20548,12 +20594,57 @@ function safeTruncate(value, maxLen) {
20548
20594
  return value.slice(0, cutIndex);
20549
20595
  }
20550
20596
 
20597
+ // ../core/dist/model-storage.js
20598
+ init_config();
20599
+
20600
+ // ../core/dist/detection-names.js
20601
+ init_types2();
20602
+ var REPORTING_ENGINE_SHORTHANDS = {
20603
+ package: "sgpk",
20604
+ amsi: "sgam",
20605
+ heuristics: "sghe",
20606
+ ml: "sgml",
20607
+ skill: "skan"
20608
+ };
20609
+ var REPORTING_DETECTION_SUFFIX = "sage";
20610
+ function formatReportingDetectionName(canonicalName, engine, optionalData = "") {
20611
+ const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
20612
+ return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
20613
+ }
20614
+ var SKILL_DETECTION_NAMES = {
20615
+ HIGH: "Other:SageSuspiciousSkill-A [Susp]",
20616
+ CRITICAL: "Other:SageMaliciousSkill-A [Trj]"
20617
+ };
20618
+ function skillDetectionName(verdict) {
20619
+ return SKILL_DETECTION_NAMES[verdict.toUpperCase()];
20620
+ }
20621
+ function isSkillId(value) {
20622
+ return /^[0-9a-f]{64}$/i.test(value);
20623
+ }
20624
+ function skillReportingDetectionName(verdict, skillId) {
20625
+ const canonicalName = skillDetectionName(verdict);
20626
+ return canonicalName && isSkillId(skillId) ? formatReportingDetectionName(canonicalName, "skill", skillId.toLowerCase()) : void 0;
20627
+ }
20628
+ function buildSkillCheckSignal(skillId, analyzerVerdict, jobId) {
20629
+ const normalizedSkillId = skillId.toLowerCase();
20630
+ const normalizedVerdict = analyzerVerdict.toUpperCase();
20631
+ const detectionName = skillReportingDetectionName(normalizedVerdict, normalizedSkillId);
20632
+ if (!detectionName)
20633
+ return void 0;
20634
+ return {
20635
+ detection_name: detectionName,
20636
+ skill_id: normalizedSkillId,
20637
+ ...jobId ? { job_id: jobId } : {},
20638
+ analyzer_verdict: normalizedVerdict
20639
+ };
20640
+ }
20641
+
20551
20642
  // ../core/dist/audit-log.js
20552
20643
  init_file_utils();
20553
20644
 
20554
20645
  // ../core/dist/jsonl-log-writer.js
20555
20646
  var import_promises2 = require("node:fs/promises");
20556
- var import_node_path2 = require("node:path");
20647
+ var import_node_path3 = require("node:path");
20557
20648
  init_config();
20558
20649
  init_file_utils();
20559
20650
  var writeQueues = /* @__PURE__ */ new Map();
@@ -20590,7 +20681,7 @@ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
20590
20681
  });
20591
20682
  }
20592
20683
  async function appendJsonlEntryNow(path2, config2, entry) {
20593
- await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(path2), { recursive: true });
20684
+ await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(path2), { recursive: true });
20594
20685
  await rotateIfNeeded(path2, config2.max_bytes, config2.max_files);
20595
20686
  await (0, import_promises2.appendFile)(path2, `${JSON.stringify(entry)}
20596
20687
  `);
@@ -20784,15 +20875,15 @@ init_types2();
20784
20875
  init_types2();
20785
20876
 
20786
20877
  // ../core/dist/version.js
20787
- var import_node_path3 = require("node:path");
20878
+ var import_node_path4 = require("node:path");
20788
20879
  var import_node_url = require("node:url");
20789
20880
  init_file_utils();
20790
20881
  var import_meta = {};
20791
20882
  function resolveVersion() {
20792
20883
  if (true)
20793
- return "0.13.0";
20884
+ return "0.15.0";
20794
20885
  try {
20795
- const pkgPath = (0, import_node_path3.join)((0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
20886
+ const pkgPath = (0, import_node_path4.join)((0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
20796
20887
  const pkg = JSON.parse(getFileContentSync(pkgPath));
20797
20888
  if (typeof pkg.version === "string")
20798
20889
  return pkg.version;
@@ -20802,9 +20893,6 @@ function resolveVersion() {
20802
20893
  }
20803
20894
  var VERSION = resolveVersion();
20804
20895
 
20805
- // ../core/dist/model-storage.js
20806
- init_config();
20807
-
20808
20896
  // ../core/dist/clients/model-downloader.js
20809
20897
  init_types2();
20810
20898
  var STALE_LOCK_MS = 60 * 60 * 1e3;
@@ -20913,7 +21001,7 @@ init_types2();
20913
21001
  // ../core/dist/extended-info.js
20914
21002
  var import_promises3 = require("node:fs/promises");
20915
21003
  var import_node_os2 = require("node:os");
20916
- var import_node_path4 = require("node:path");
21004
+ var import_node_path5 = require("node:path");
20917
21005
  init_file_utils();
20918
21006
  init_types2();
20919
21007
  var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
@@ -20979,8 +21067,8 @@ function sanitizeDocument(parsed, logger2) {
20979
21067
  return out;
20980
21068
  }
20981
21069
  async function loadExtendedInfo(sageDirPath, logger2 = nullLogger) {
20982
- const sageDir = sageDirPath ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".sage");
20983
- const filePath = (0, import_node_path4.join)(sageDir, EXTENDED_INFO_FILENAME);
21070
+ const sageDir = sageDirPath ?? (0, import_node_path5.join)((0, import_node_os2.homedir)(), ".sage");
21071
+ const filePath = (0, import_node_path5.join)(sageDir, EXTENDED_INFO_FILENAME);
20984
21072
  const cached2 = cache.get(filePath);
20985
21073
  if (cached2)
20986
21074
  return cached2.value;
@@ -21078,14 +21166,14 @@ function mergeExtendedInfo(envelope, extendedInfo) {
21078
21166
  }
21079
21167
 
21080
21168
  // ../core/dist/installation-id.js
21081
- var import_node_crypto = require("node:crypto");
21169
+ var import_node_crypto2 = require("node:crypto");
21082
21170
  var import_promises4 = require("node:fs/promises");
21083
- var import_node_path5 = require("node:path");
21171
+ var import_node_path6 = require("node:path");
21084
21172
  init_config();
21085
21173
  init_file_utils();
21086
21174
  async function getInstallationId(sageDirPath) {
21087
21175
  const sageDir = sageDirPath ?? resolvePath("~/.sage");
21088
- const idPath = (0, import_node_path5.join)(sageDir, "installation-id");
21176
+ const idPath = (0, import_node_path6.join)(sageDir, "installation-id");
21089
21177
  let fileExists = false;
21090
21178
  try {
21091
21179
  const existing = await getFileContent(idPath, "utf-8");
@@ -21096,7 +21184,7 @@ async function getInstallationId(sageDirPath) {
21096
21184
  } catch {
21097
21185
  }
21098
21186
  try {
21099
- const id = (0, import_node_crypto.randomUUID)();
21187
+ const id = (0, import_node_crypto2.randomUUID)();
21100
21188
  await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
21101
21189
  await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
21102
21190
  return id;
@@ -21128,6 +21216,12 @@ init_types2();
21128
21216
  // ../core/dist/evaluator.js
21129
21217
  init_config();
21130
21218
 
21219
+ // ../core/dist/extractors.js
21220
+ var MAX_CONTENT_SIZE = 64 * 1024;
21221
+
21222
+ // ../core/dist/deobfuscate.js
21223
+ var REPLACEMENT_CHAR = String.fromCharCode(65533);
21224
+
21131
21225
  // ../core/dist/exceptions.js
21132
21226
  init_config();
21133
21227
  init_file_utils();
@@ -21140,9 +21234,6 @@ init_types2();
21140
21234
  // ../core/dist/exceptions.js
21141
21235
  init_types2();
21142
21236
 
21143
- // ../core/dist/extractors.js
21144
- var MAX_CONTENT_SIZE = 64 * 1024;
21145
-
21146
21237
  // ../core/dist/package-checker.js
21147
21238
  init_types2();
21148
21239
 
@@ -21150,17 +21241,43 @@ init_types2();
21150
21241
  init_config();
21151
21242
  init_file_utils();
21152
21243
 
21244
+ // ../core/dist/submit-limits.js
21245
+ init_config();
21246
+ init_file_utils();
21247
+ init_types2();
21248
+ var SUBMIT_WINDOW_MS = 24 * 60 * 60 * 1e3;
21249
+
21153
21250
  // ../core/dist/threat-loader.js
21154
21251
  var import_yaml2 = __toESM(require_dist(), 1);
21252
+ init_config();
21155
21253
  init_file_utils();
21156
21254
  init_types2();
21157
21255
 
21158
21256
  // ../core/dist/evaluator.js
21159
21257
  init_types2();
21258
+ function hasReportableDetectionSignals(value) {
21259
+ if (!value || typeof value !== "object" || Array.isArray(value))
21260
+ return false;
21261
+ const signals = value;
21262
+ const hasEntries = (key) => {
21263
+ const entries = signals[key];
21264
+ return Array.isArray(entries) && entries.length > 0;
21265
+ };
21266
+ return Boolean(hasEntries("heuristics") || hasEntries("url_checks") || hasEntries("file_checks") || hasEntries("package_checks") || hasEntries("amsi_checks") || Array.isArray(signals.pi_checks) && signals.pi_checks.some((result) => {
21267
+ if (!result || typeof result !== "object" || Array.isArray(result))
21268
+ return false;
21269
+ const risk = result.risk;
21270
+ return typeof risk === "number" && Number.isFinite(risk) && risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
21271
+ }));
21272
+ }
21160
21273
 
21161
21274
  // ../core/dist/index.js
21162
21275
  init_file_utils();
21163
21276
 
21277
+ // ../core/dist/format.js
21278
+ var FP_REPORT_TOOL_NAME = "sage_report_false_positive";
21279
+ var SESSION_START_FP_HINT = `If this is a false positive, ask your agent to report it with the ${FP_REPORT_TOOL_NAME} MCP tool.`;
21280
+
21164
21281
  // ../core/dist/guard.js
21165
21282
  init_config();
21166
21283
  init_types2();
@@ -21311,11 +21428,11 @@ function createOperationalLogger(config2, runtime) {
21311
21428
  }
21312
21429
 
21313
21430
  // ../core/dist/plugin-scan-cache.js
21314
- var import_node_path6 = require("node:path");
21431
+ var import_node_path7 = require("node:path");
21315
21432
  init_config();
21316
21433
  init_file_utils();
21317
21434
  init_types2();
21318
- var DEFAULT_CACHE_PATH = (0, import_node_path6.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
21435
+ var DEFAULT_CACHE_PATH = (0, import_node_path7.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
21319
21436
 
21320
21437
  // ../core/dist/plugin-scanner.js
21321
21438
  init_config();
@@ -21331,9 +21448,119 @@ init_types2();
21331
21448
  var PENDING_TTL_MS = 60 * 60 * 1e3;
21332
21449
 
21333
21450
  // ../core/dist/skill-verdict-cache.js
21451
+ var import_node_path8 = require("node:path");
21334
21452
  init_config();
21335
21453
  init_file_utils();
21336
21454
  init_types2();
21455
+ var SCHEMA_VERSION = 1;
21456
+ var DEFAULT_VERDICT_TTL_MS = MS_PER_DAY;
21457
+ function parseSources(raw) {
21458
+ if (!Array.isArray(raw))
21459
+ return void 0;
21460
+ const sources = [];
21461
+ for (const item of raw) {
21462
+ if (!item || typeof item !== "object")
21463
+ continue;
21464
+ const entry = item;
21465
+ const source = {
21466
+ agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
21467
+ containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
21468
+ };
21469
+ if (source.agentRuntime || source.containerKey)
21470
+ sources.push(source);
21471
+ }
21472
+ return sources.length > 0 ? sources : void 0;
21473
+ }
21474
+ function defaultCachePath2() {
21475
+ return (0, import_node_path8.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
21476
+ }
21477
+ async function loadSkillVerdictCacheInternal(cachePath, ttlMs, logger2, pruneOnLoad) {
21478
+ let raw;
21479
+ try {
21480
+ raw = await getFileContent(cachePath);
21481
+ } catch {
21482
+ return { entries: {} };
21483
+ }
21484
+ try {
21485
+ const data = JSON.parse(raw);
21486
+ const rawEntries = data.entries ?? {};
21487
+ const now = Date.now();
21488
+ const entries = {};
21489
+ let pruned = 0;
21490
+ for (const [skillId, entry] of Object.entries(rawEntries)) {
21491
+ const analyzedAt = entry.analyzed_at;
21492
+ if (!analyzedAt)
21493
+ continue;
21494
+ const ts = Date.parse(analyzedAt);
21495
+ if (!isFreshTimestamp(ts, ttlMs, now)) {
21496
+ pruned++;
21497
+ continue;
21498
+ }
21499
+ entries[skillId] = {
21500
+ entryId: typeof entry.entry_id === "string" ? entry.entry_id : void 0,
21501
+ jobId: typeof entry.job_id === "string" ? entry.job_id : void 0,
21502
+ verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
21503
+ summary: typeof entry.summary === "string" ? entry.summary : void 0,
21504
+ skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
21505
+ sources: parseSources(entry.sources),
21506
+ analyzedAt
21507
+ };
21508
+ }
21509
+ const cache2 = { entries };
21510
+ if (pruneOnLoad && pruned > 0) {
21511
+ logger2.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
21512
+ await saveSkillVerdictCache(cache2, cachePath, logger2);
21513
+ }
21514
+ return cache2;
21515
+ } catch (e) {
21516
+ logger2.warn(`Failed to load verdict cache from ${cachePath}`, { error: String(e) });
21517
+ return { entries: {} };
21518
+ }
21519
+ }
21520
+ async function loadSkillVerdictCacheReadOnly(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger2 = nullLogger) {
21521
+ return loadSkillVerdictCacheInternal(cachePath, ttlMs, logger2, false);
21522
+ }
21523
+ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger2 = nullLogger) {
21524
+ try {
21525
+ const data = {
21526
+ schema_version: SCHEMA_VERSION,
21527
+ entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
21528
+ skillId,
21529
+ {
21530
+ entry_id: v.entryId,
21531
+ job_id: v.jobId,
21532
+ verdict: v.verdict,
21533
+ summary: v.summary,
21534
+ skill_name: v.skillName,
21535
+ sources: v.sources?.map((s) => ({
21536
+ agent_runtime: s.agentRuntime,
21537
+ container_key: s.containerKey
21538
+ })),
21539
+ analyzed_at: v.analyzedAt
21540
+ }
21541
+ ]))
21542
+ };
21543
+ await atomicWriteJson(cachePath, data);
21544
+ return true;
21545
+ } catch (e) {
21546
+ logger2.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
21547
+ return false;
21548
+ }
21549
+ }
21550
+
21551
+ // ../core/dist/skill-verdict-record.js
21552
+ var import_node_crypto3 = require("node:crypto");
21553
+ var SKILL_ENTRY_ID_NAMESPACE = Buffer.from("75bba79a376142a7bacbf95d791126d3", "hex");
21554
+ function legacySkillEntryId(skillId, analyzedAt) {
21555
+ const bytes = (0, import_node_crypto3.createHash)("sha1").update(SKILL_ENTRY_ID_NAMESPACE).update(`${skillId.toLowerCase()}:${analyzedAt}`).digest().subarray(0, 16);
21556
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
21557
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
21558
+ const hex = bytes.toString("hex");
21559
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
21560
+ }
21561
+ function resolveSkillVerdictEntryId(skillId, entry) {
21562
+ return entry.entryId ?? legacySkillEntryId(skillId, entry.analyzedAt);
21563
+ }
21337
21564
 
21338
21565
  // ../core/dist/plugin-scanner.js
21339
21566
  init_types2();
@@ -21341,12 +21568,12 @@ var MAX_FILE_SIZE = 512 * 1024;
21341
21568
 
21342
21569
  // ../core/dist/product-version.js
21343
21570
  var import_node_fs = require("node:fs");
21344
- var import_node_path7 = __toESM(require("node:path"), 1);
21571
+ var import_node_path9 = __toESM(require("node:path"), 1);
21345
21572
  function readProductJsonVersion(appRoot) {
21346
21573
  if (!appRoot)
21347
21574
  return "unknown";
21348
21575
  try {
21349
- const productJsonPath = import_node_path7.default.join(appRoot, "product.json");
21576
+ const productJsonPath = import_node_path9.default.join(appRoot, "product.json");
21350
21577
  const raw = (0, import_node_fs.readFileSync)(productJsonPath, "utf8");
21351
21578
  const parsed = JSON.parse(raw);
21352
21579
  const version2 = parsed.version;
@@ -31409,7 +31636,7 @@ var StdioServerTransport = class {
31409
31636
  };
31410
31637
 
31411
31638
  // ../mcp/dist/tools/false-positive.js
31412
- var import_node_crypto2 = require("node:crypto");
31639
+ var import_node_crypto4 = require("node:crypto");
31413
31640
  init_zod();
31414
31641
 
31415
31642
  // ../mcp/dist/tools/utils.js
@@ -31445,7 +31672,63 @@ function readContentSnippet(entry) {
31445
31672
  function isRuntimeVerdictEntry(v) {
31446
31673
  if (!v || typeof v !== "object" || Array.isArray(v))
31447
31674
  return false;
31448
- return v.type === "runtime_verdict";
31675
+ const entry = v;
31676
+ return entry.type === "runtime_verdict" && (entry.custom_threat_matched !== true || entry.non_custom_detection_matched === true || hasReportableDetectionSignals(parseRuntimeAuditSignals(entry.signals)));
31677
+ }
31678
+ function isAnalyzedSkillVerdictEntry(v) {
31679
+ if (!v || typeof v !== "object" || Array.isArray(v))
31680
+ return false;
31681
+ const entry = v;
31682
+ return entry.type === "skill_verdict" && entry.status === "analyzed" && isSkillId(asString(entry.skill_id) ?? "");
31683
+ }
31684
+ function isReportableSkillVerdictEntry(v) {
31685
+ if (!isAnalyzedSkillVerdictEntry(v))
31686
+ return false;
31687
+ const entry = v;
31688
+ const verdict = asString(entry.verdict)?.toUpperCase();
31689
+ return (verdict === "HIGH" || verdict === "CRITICAL") && asString(entry.entry_id) !== void 0;
31690
+ }
31691
+ function latestReportableSkillVerdicts(entries, ttlMs, now = Date.now()) {
31692
+ const latestBySkillId = /* @__PURE__ */ new Map();
31693
+ for (const entry of entries) {
31694
+ if (!isAnalyzedSkillVerdictEntry(entry))
31695
+ continue;
31696
+ const timestamp = Date.parse(asString(entry.timestamp) ?? "");
31697
+ if (!isFreshTimestamp(timestamp, ttlMs, now))
31698
+ continue;
31699
+ const normalizedSkillId = entry.skill_id.toLowerCase();
31700
+ const current = latestBySkillId.get(normalizedSkillId);
31701
+ if (current && timestamp < current.timestamp)
31702
+ continue;
31703
+ latestBySkillId.delete(normalizedSkillId);
31704
+ latestBySkillId.set(normalizedSkillId, { entry, timestamp });
31705
+ }
31706
+ return [...latestBySkillId.values()].map(({ entry }) => entry).filter(isReportableSkillVerdictEntry);
31707
+ }
31708
+ async function cachedSkillVerdictEntries(config2, logger2) {
31709
+ const cache2 = await loadSkillVerdictCacheReadOnly(void 0, skillCacheTtlMs(config2), logger2);
31710
+ return Object.entries(cache2.entries).flatMap(([skillId, entry]) => {
31711
+ if (!isSkillId(skillId))
31712
+ return [];
31713
+ if (!entry.verdict)
31714
+ return [];
31715
+ const normalizedSkillId = skillId.toLowerCase();
31716
+ const analyzerVerdict = entry.verdict.toUpperCase();
31717
+ const signal = buildSkillCheckSignal(normalizedSkillId, analyzerVerdict, entry.jobId);
31718
+ return [
31719
+ {
31720
+ type: "skill_verdict",
31721
+ entry_id: resolveSkillVerdictEntryId(normalizedSkillId, entry),
31722
+ timestamp: entry.analyzedAt,
31723
+ agent_runtime: entry.sources?.[0]?.agentRuntime,
31724
+ skill_id: normalizedSkillId,
31725
+ job_id: entry.jobId,
31726
+ status: "analyzed",
31727
+ verdict: analyzerVerdict,
31728
+ signals: signal ? { skill_checks: [signal] } : void 0
31729
+ }
31730
+ ];
31731
+ });
31449
31732
  }
31450
31733
  function getConversationId(entry) {
31451
31734
  return asString(entry.conversation_id) ?? asString(entry.session_id);
@@ -31453,6 +31736,10 @@ function getConversationId(entry) {
31453
31736
  function getEntryId(entry) {
31454
31737
  return asString(entry.entry_id);
31455
31738
  }
31739
+ function getEntryTimestamp(entry) {
31740
+ const timestamp = Date.parse(asString(entry.timestamp) ?? "");
31741
+ return Number.isFinite(timestamp) ? timestamp : 0;
31742
+ }
31456
31743
  function asHookType(value) {
31457
31744
  const s = asString(value);
31458
31745
  if (s === "PreToolUse" || s === "PostToolUse" || s === "SessionStart" || s === "GatewayStart" || s === "BeforeAgentStart" || s === "MessagesTransform") {
@@ -31473,9 +31760,10 @@ async function postMultipart(endpoint, payload, timeoutSeconds) {
31473
31760
  const bodyText = await res.text().catch(() => "");
31474
31761
  return { ok: res.ok, status: res.status, bodyText };
31475
31762
  }
31763
+ var MAX_AUDIT_SCAN_ENTRIES = 5e3;
31476
31764
  var ListInputSchema = external_exports.object({
31477
31765
  conversation_id: external_exports.string().optional().describe("Conversation id to filter audit entries; if omitted, uses the most recent one."),
31478
- limit: external_exports.number().int().min(1).max(5e3).optional().describe("Max entries to scan and return.")
31766
+ limit: external_exports.number().int().min(1).max(MAX_AUDIT_SCAN_ENTRIES).optional().describe("Max merged entries to return.")
31479
31767
  });
31480
31768
  var MAX_IMPLICIT_ENTRIES = 3;
31481
31769
  var MAX_EXPLICIT_ENTRIES = 10;
@@ -31484,10 +31772,10 @@ var ReportInputSchema = external_exports.object({
31484
31772
  description: external_exports.string().min(1).describe("Short description of what is a false positive."),
31485
31773
  reasoning: external_exports.string().min(1).describe("Reasoning for why this should be treated as a false positive."),
31486
31774
  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'."),
31487
- 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.`),
31775
+ 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.`),
31488
31776
  dry_run: external_exports.boolean().optional().describe("If true, do not POST; just show the payload.")
31489
31777
  });
31490
- function parseAuditSignals(raw, options = {}) {
31778
+ function parseRuntimeAuditSignals(raw, options = {}) {
31491
31779
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
31492
31780
  return {};
31493
31781
  const includePiContentSnippet = options.includePiContentSnippet ?? true;
@@ -31596,7 +31884,7 @@ function registerFalsePositiveTools(server, opts) {
31596
31884
  const branding = opts.branding ?? defaultBranding;
31597
31885
  server.registerTool("sage_list_audit_entries", {
31598
31886
  title: `${branding.name}: List Audit Entries`,
31599
- description: `List recent ${branding.name} audit log entries, optionally scoped to a conversation id. Useful for selecting entry_ids to report as false positives.`,
31887
+ 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.`,
31600
31888
  inputSchema: ListInputSchema
31601
31889
  }, async ({ conversation_id, limit }) => {
31602
31890
  const complete = (result, data = {}) => {
@@ -31614,33 +31902,59 @@ function registerFalsePositiveTools(server, opts) {
31614
31902
  });
31615
31903
  try {
31616
31904
  const config2 = await loadConfig(void 0, logger2);
31617
- const scanLimit = limit ?? 500;
31618
- const entries = await getRecentEntries(config2.logging, scanLimit);
31905
+ const outputLimit = limit ?? 500;
31906
+ const entries = await getRecentEntries(config2.logging, MAX_AUDIT_SCAN_ENTRIES);
31619
31907
  const runtime = entries.filter(isRuntimeVerdictEntry);
31908
+ const cachedSkills = await cachedSkillVerdictEntries(config2, logger2);
31909
+ const skills = latestReportableSkillVerdicts([...entries, ...cachedSkills], skillCacheTtlMs(config2));
31620
31910
  const inferredConversationId = conversation_id ?? getConversationId(runtime.slice().reverse().find((e) => !!getConversationId(e)) ?? {});
31621
- if (!inferredConversationId) {
31622
- complete("skipped", { skippedReason: "no_runtime_entries", entriesReturned: 0 });
31623
- return textResult("No runtime_verdict entries found in the audit log.", true);
31624
- }
31625
- const filtered = runtime.filter((e) => getConversationId(e) === inferredConversationId);
31626
- const out = filtered.map((e) => ({
31627
- entry_id: getEntryId(e),
31628
- timestamp: asString(e.timestamp),
31629
- conversation_id: inferredConversationId,
31630
- agent_runtime: asString(e.agent_runtime),
31631
- tool_name: asString(e.tool_name),
31632
- tool_input_summary: asString(e.tool_input_summary),
31633
- verdict: asString(e.verdict),
31634
- severity: asString(e.severity),
31635
- source: asString(e.source),
31636
- user_override: e.user_override,
31637
- // PI snippets are untrusted fetched content. Exclude their raw
31638
- // text—including the generic top-level content_snippet - from this
31639
- // model-visible response. Retain it only for the backend-only
31640
- // false-positive report below.
31641
- signals: parseAuditSignals(e.signals, { includePiContentSnippet: false }),
31642
- content: readContent(e)
31643
- }));
31911
+ if (!inferredConversationId && skills.length === 0) {
31912
+ complete("skipped", { skippedReason: "no_reportable_entries", entriesReturned: 0 });
31913
+ return textResult("No reportable runtime or skill verdict entries found.", true);
31914
+ }
31915
+ const filteredRuntime = inferredConversationId ? runtime.filter((e) => getConversationId(e) === inferredConversationId) : [];
31916
+ const selected = [
31917
+ ...filteredRuntime,
31918
+ ...skills
31919
+ ].sort((a, b) => getEntryTimestamp(a) - getEntryTimestamp(b)).slice(-outputLimit);
31920
+ const out = selected.map((entry) => {
31921
+ if (isReportableSkillVerdictEntry(entry)) {
31922
+ const skillId = entry.skill_id;
31923
+ const analyzerVerdict = asString(entry.verdict)?.toUpperCase() ?? "";
31924
+ const signal = buildSkillCheckSignal(skillId, analyzerVerdict, asString(entry.job_id));
31925
+ return {
31926
+ type: "skill_verdict",
31927
+ entry_id: getEntryId(entry),
31928
+ timestamp: asString(entry.timestamp),
31929
+ agent_runtime: asString(entry.agent_runtime),
31930
+ skill_id: skillId.toLowerCase(),
31931
+ job_id: asString(entry.job_id),
31932
+ status: asString(entry.status),
31933
+ analyzer_verdict: analyzerVerdict,
31934
+ signals: signal ? { skill_checks: [signal] } : {}
31935
+ };
31936
+ }
31937
+ const runtimeEntry = entry;
31938
+ return {
31939
+ type: "runtime_verdict",
31940
+ entry_id: getEntryId(runtimeEntry),
31941
+ timestamp: asString(runtimeEntry.timestamp),
31942
+ conversation_id: inferredConversationId,
31943
+ agent_runtime: asString(runtimeEntry.agent_runtime),
31944
+ tool_name: asString(runtimeEntry.tool_name),
31945
+ tool_input_summary: asString(runtimeEntry.tool_input_summary),
31946
+ verdict: asString(runtimeEntry.verdict),
31947
+ severity: asString(runtimeEntry.severity),
31948
+ source: asString(runtimeEntry.source),
31949
+ user_override: runtimeEntry.user_override,
31950
+ // PI snippets are untrusted fetched content. Exclude their raw
31951
+ // text from this model-visible response.
31952
+ signals: parseRuntimeAuditSignals(runtimeEntry.signals, {
31953
+ includePiContentSnippet: false
31954
+ }),
31955
+ content: readContent(runtimeEntry)
31956
+ };
31957
+ });
31644
31958
  complete("completed", {
31645
31959
  conversationId: inferredConversationId,
31646
31960
  entriesReturned: out.length
@@ -31657,11 +31971,11 @@ function registerFalsePositiveTools(server, opts) {
31657
31971
  });
31658
31972
  server.registerTool("sage_report_false_positive", {
31659
31973
  title: `${branding.name}: Report False Positive`,
31660
- description: `Report ${branding.name} audit log entries as false positives to the backend, scoped to the current conversation.
31974
+ 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.
31661
31975
 
31662
31976
  USAGE \u2014 IMPORTANT:
31663
- 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.
31664
- 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.
31977
+ 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.
31978
+ 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.
31665
31979
  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.`,
31666
31980
  inputSchema: ReportInputSchema
31667
31981
  }, async ({ conversation_id, description, reasoning, agent_runtime_version, entry_ids, dry_run }) => {
@@ -31685,31 +31999,35 @@ USAGE \u2014 IMPORTANT:
31685
31999
  const config2 = await loadConfig(void 0, logger2);
31686
32000
  const endpoint = resolveEndpoint("/v2/fp-report");
31687
32001
  const timeoutSeconds = Number(process.env.SAGE_FALSE_POSITIVE_TIMEOUT_SECONDS) || 5;
31688
- const scanLimit = 2e3;
31689
- const entries = await getRecentEntries(config2.logging, scanLimit);
32002
+ const entries = await getRecentEntries(config2.logging, MAX_AUDIT_SCAN_ENTRIES);
31690
32003
  const runtime = entries.filter(isRuntimeVerdictEntry);
32004
+ const cachedSkills = await cachedSkillVerdictEntries(config2, logger2);
32005
+ const skills = latestReportableSkillVerdicts([...entries, ...cachedSkills], skillCacheTtlMs(config2));
31691
32006
  const inferredConversationId = conversation_id ?? getConversationId(runtime.slice().reverse().find((e) => !!getConversationId(e)) ?? {});
31692
- if (!inferredConversationId) {
31693
- complete("skipped", { skippedReason: "no_runtime_entries", reportsCount: 0 });
31694
- return textResult("No runtime_verdict entries found in the audit log.", true);
31695
- }
31696
- let filtered = runtime.filter((e) => getConversationId(e) === inferredConversationId);
32007
+ let filtered;
31697
32008
  if (entry_ids && entry_ids.length > 0) {
31698
- const wanted = new Set(entry_ids);
31699
- filtered = filtered.filter((e) => {
31700
- const id = getEntryId(e);
31701
- return id ? wanted.has(id) : false;
31702
- });
31703
- if (filtered.length > MAX_EXPLICIT_ENTRIES) {
32009
+ if (entry_ids.length > MAX_EXPLICIT_ENTRIES) {
31704
32010
  complete("failed_validation", {
31705
32011
  conversationId: inferredConversationId,
31706
- reportsCount: filtered.length,
32012
+ entryIdsCount: entry_ids.length,
31707
32013
  maxEntries: MAX_EXPLICIT_ENTRIES
31708
32014
  });
31709
- return textResult(`Too many entry_ids (${filtered.length}). Provide at most ${MAX_EXPLICIT_ENTRIES} specific entry_ids per report call.`, true);
32015
+ return textResult(`Too many entry_ids (${entry_ids.length}). Provide at most ${MAX_EXPLICIT_ENTRIES} specific entry_ids per report call.`, true);
31710
32016
  }
32017
+ const wanted = new Set(entry_ids);
32018
+ const scopedRuntime = inferredConversationId ? runtime.filter((e) => getConversationId(e) === inferredConversationId) : [];
32019
+ filtered = [...scopedRuntime, ...skills].filter((e) => {
32020
+ const id = getEntryId(e);
32021
+ return id !== void 0 && wanted.has(id);
32022
+ });
31711
32023
  } else {
31712
- filtered = filtered.filter((e) => {
32024
+ if (!inferredConversationId) {
32025
+ complete("skipped", { skippedReason: "no_runtime_entries", reportsCount: 0 });
32026
+ return textResult("No runtime_verdict entries found. Skill verdicts must be reported with explicit entry_ids.", true);
32027
+ }
32028
+ filtered = runtime.filter((e) => {
32029
+ if (getConversationId(e) !== inferredConversationId)
32030
+ return false;
31713
32031
  const v = asString(e.verdict);
31714
32032
  return v === "deny" || v === "ask";
31715
32033
  });
@@ -31724,7 +32042,7 @@ USAGE \u2014 IMPORTANT:
31724
32042
  skippedReason: "no_matching_entry_ids",
31725
32043
  reportsCount: 0
31726
32044
  });
31727
- return textResult("No matching audit entries found for the provided entry_ids in this conversation. Run sage_list_audit_entries first.", true);
32045
+ return textResult("No matching reportable audit entries found for the provided entry_ids. Run sage_list_audit_entries first.", true);
31728
32046
  }
31729
32047
  complete("skipped", {
31730
32048
  conversationId: inferredConversationId,
@@ -31750,19 +32068,22 @@ USAGE \u2014 IMPORTANT:
31750
32068
  ${reasoning}`;
31751
32069
  const extendedInfo = await loadExtendedInfo(void 0, logger2).catch(() => null);
31752
32070
  const reports = filtered.map((e) => {
32071
+ const isSkill = isReportableSkillVerdictEntry(e);
32072
+ const runtimeEntry = isSkill ? void 0 : e;
31753
32073
  const entry_id = getEntryId(e);
31754
32074
  const timestamp = asString(e.timestamp) ?? (/* @__PURE__ */ new Date()).toISOString();
31755
32075
  const agent_runtime = asString(e.agent_runtime) ?? "unknown";
31756
- const tool_type = asString(e.tool_name) ?? "Unknown";
31757
- const verdict = asString(e.verdict) ?? "deny";
31758
- const wasAllowed = verdict === "allow" || e.user_override === true;
32076
+ const analyzerVerdict = isSkill ? asString(e.verdict)?.toUpperCase() : void 0;
32077
+ const tool_type = isSkill ? "Skill" : asString(runtimeEntry?.tool_name) ?? "Unknown";
32078
+ const verdict = isSkill ? analyzerVerdict === "CRITICAL" ? "deny" : "ask" : asString(e.verdict) ?? "deny";
32079
+ const wasAllowed = !isSkill && (verdict === "allow" || runtimeEntry?.user_override === true);
31759
32080
  const user_action = wasAllowed ? "allowed" : "blocked";
31760
- const hook_type = asHookType(e.hook_type) ?? "PreToolUse";
31761
- const signals = parseAuditSignals(e.signals, {
32081
+ const hook_type = isSkill ? "SessionStart" : asHookType(runtimeEntry?.hook_type) ?? "PreToolUse";
32082
+ const signals = parseRuntimeAuditSignals(e.signals, {
31762
32083
  includePiContentSnippet: !dry_run
31763
32084
  });
31764
- const content = readContent(e);
31765
- const contentSnippet = readContentSnippet(e);
32085
+ const content = runtimeEntry ? readContent(runtimeEntry) : {};
32086
+ const contentSnippet = runtimeEntry ? readContentSnippet(runtimeEntry) : void 0;
31766
32087
  const bestEffortSignals = {};
31767
32088
  if (signals.heuristics)
31768
32089
  bestEffortSignals.heuristics = signals.heuristics;
@@ -31776,6 +32097,11 @@ ${reasoning}`;
31776
32097
  bestEffortSignals.pi_checks = signals.pi_checks;
31777
32098
  if (signals.amsi_checks)
31778
32099
  bestEffortSignals.amsi_checks = signals.amsi_checks;
32100
+ if (isSkill) {
32101
+ const signal = analyzerVerdict ? buildSkillCheckSignal(e.skill_id, analyzerVerdict, asString(e.job_id)) : void 0;
32102
+ if (signal)
32103
+ bestEffortSignals.skill_checks = [signal];
32104
+ }
31779
32105
  const baseReportPayload = {
31780
32106
  ...buildSageProxyEnvelope({
31781
32107
  iid,
@@ -31791,11 +32117,11 @@ ${reasoning}`;
31791
32117
  user_action,
31792
32118
  timestamp,
31793
32119
  ...Object.keys(bestEffortSignals).length > 0 ? { signals: bestEffortSignals } : {},
31794
- content,
32120
+ ...Object.keys(content).length > 0 ? { content } : {},
31795
32121
  ...contentSnippet && !dry_run ? { content_snippet: contentSnippet } : {}
31796
32122
  },
31797
32123
  comment,
31798
- event_id: entry_id ?? (0, import_node_crypto2.randomUUID)()
32124
+ event_id: entry_id ?? (0, import_node_crypto4.randomUUID)()
31799
32125
  };
31800
32126
  const reportPayload = mergeExtendedInfo(baseReportPayload, extendedInfo);
31801
32127
  return { entry_id, payload: reportPayload };
@@ -31849,7 +32175,8 @@ ${JSON.stringify(summary, null, 2)}`, true);
31849
32175
  conversationId: inferredConversationId,
31850
32176
  reported: filtered.length
31851
32177
  });
31852
- return textResult(`Reported ${filtered.length} audit entr${filtered.length === 1 ? "y" : "ies"} for conversation_id=${inferredConversationId}.`);
32178
+ const scope = inferredConversationId ? ` for conversation_id=${inferredConversationId}` : "";
32179
+ return textResult(`Reported ${filtered.length} audit entr${filtered.length === 1 ? "y" : "ies"}${scope}.`);
31853
32180
  } catch (e) {
31854
32181
  logger2.warn("MCP tool failed", {
31855
32182
  toolName: "sage_report_false_positive",
@@ -31897,7 +32224,7 @@ async function main() {
31897
32224
  );
31898
32225
  const branding = resolveBranding(config2.brand_key, logger);
31899
32226
  await runSageMcpServerStdio({
31900
- version: "0.13.0",
32227
+ version: "0.15.0",
31901
32228
  logger,
31902
32229
  branding
31903
32230
  });