@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.
- package/dist/mcp-server.cjs +411 -88
- package/dist/mcp-server.cjs.map +4 -4
- package/dist/tool-handler.d.ts.map +1 -1
- package/dist/tool-handler.js +5 -3
- package/dist/tool-handler.js.map +1 -1
- package/package.json +3 -3
- package/resources/threats/_macros.yaml +193 -0
- package/resources/threats/agent-layer.yaml +19 -1
- package/resources/threats/commands.yaml +165 -31
- package/resources/threats/credentials.yaml +24 -4
- package/resources/threats/dummy.yaml +30 -0
- package/resources/threats/files.yaml +38 -0
- package/resources/threats/mac-commands.yaml +118 -27
- package/resources/threats/mac-credentials.yaml +34 -8
- package/resources/threats/mac-files.yaml +20 -0
- package/resources/threats/mac-obfuscation.yaml +28 -6
- package/resources/threats/mac-persistence.yaml +30 -4
- package/resources/threats/mac-supply-chain.yaml +12 -3
- package/resources/threats/mitre.yaml +325 -62
- package/resources/threats/obfuscation.yaml +33 -8
- package/resources/threats/persistence.yaml +40 -5
- package/resources/threats/prompt-injection.yaml +58 -0
- package/resources/threats/self-defense.yaml +15 -2
- package/resources/threats/supply_chain.yaml +6 -1
- package/resources/threats/urls.yaml +10 -0
- package/resources/threats/win-commands.yaml +294 -60
- package/resources/threats/win-credentials.yaml +68 -9
- package/resources/threats/win-files.yaml +20 -0
- package/resources/threats/win-obfuscation.yaml +56 -8
- package/resources/threats/win-persistence.yaml +45 -7
- package/resources/threats/win-supply-chain.yaml +5 -1
package/dist/mcp-server.cjs
CHANGED
|
@@ -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().
|
|
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,
|
|
4398
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "config.json");
|
|
4352
4399
|
}
|
|
4353
4400
|
function defaultCachePath() {
|
|
4354
|
-
return (0,
|
|
4401
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
|
|
4355
4402
|
}
|
|
4356
4403
|
function defaultExceptionsPath() {
|
|
4357
|
-
return (0,
|
|
4404
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "exceptions.json");
|
|
4358
4405
|
}
|
|
4359
4406
|
function defaultAuditPath() {
|
|
4360
|
-
return (0,
|
|
4407
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "audit.jsonl");
|
|
4361
4408
|
}
|
|
4362
4409
|
function defaultOperationalLogPath() {
|
|
4363
|
-
return (0,
|
|
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,
|
|
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,
|
|
4420
|
+
const rel = (0, import_node_path2.relative)(baseDir, targetPath);
|
|
4374
4421
|
if (rel === "")
|
|
4375
4422
|
return true;
|
|
4376
|
-
if ((0,
|
|
4423
|
+
if ((0, import_node_path2.isAbsolute)(rel))
|
|
4377
4424
|
return false;
|
|
4378
|
-
return rel !== ".." && !rel.startsWith(`..${
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
20879
|
+
return "0.14.0";
|
|
20792
20880
|
try {
|
|
20793
|
-
const pkgPath = (0,
|
|
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
|
|
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,
|
|
20981
|
-
const filePath = (0,
|
|
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
|
|
21167
|
+
var import_node_crypto2 = require("node:crypto");
|
|
21080
21168
|
var import_promises4 = require("node:fs/promises");
|
|
21081
|
-
var
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
31599
|
-
const entries = await getRecentEntries(config2.logging,
|
|
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: "
|
|
31604
|
-
return textResult("No
|
|
31605
|
-
}
|
|
31606
|
-
const
|
|
31607
|
-
const
|
|
31608
|
-
|
|
31609
|
-
|
|
31610
|
-
|
|
31611
|
-
|
|
31612
|
-
|
|
31613
|
-
|
|
31614
|
-
|
|
31615
|
-
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
|
|
31619
|
-
|
|
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
|
|
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
|
|
31641
|
-
2. ALWAYS pass the \`entry_ids\` parameter with just the relevant
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31980
|
+
entryIdsCount: entry_ids.length,
|
|
31684
31981
|
maxEntries: MAX_EXPLICIT_ENTRIES
|
|
31685
31982
|
});
|
|
31686
|
-
return textResult(`Too many entry_ids (${
|
|
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
|
-
|
|
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
|
|
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
|
|
31734
|
-
const
|
|
31735
|
-
const
|
|
31736
|
-
const
|
|
31737
|
-
const
|
|
31738
|
-
const
|
|
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,
|
|
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
|
-
|
|
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.
|
|
32195
|
+
version: "0.14.0",
|
|
31873
32196
|
logger,
|
|
31874
32197
|
branding
|
|
31875
32198
|
});
|