@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.
- package/README.md +14 -2
- package/dist/extractors.d.ts +7 -0
- package/dist/extractors.d.ts.map +1 -1
- package/dist/extractors.js +89 -16
- package/dist/extractors.js.map +1 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -320
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +425 -98
- package/dist/mcp-server.cjs.map +4 -4
- package/dist/plugin-discovery.d.ts +7 -0
- package/dist/plugin-discovery.d.ts.map +1 -1
- package/dist/plugin-discovery.js +46 -11
- package/dist/plugin-discovery.js.map +1 -1
- package/dist/shared.d.ts +63 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +352 -0
- package/dist/shared.js.map +1 -0
- package/dist/tool-handler.d.ts +6 -0
- package/dist/tool-handler.d.ts.map +1 -1
- package/dist/tool-handler.js +22 -4
- package/dist/tool-handler.js.map +1 -1
- package/dist/v2-types.d.ts +95 -0
- package/dist/v2-types.d.ts.map +1 -0
- package/dist/v2-types.js +10 -0
- package/dist/v2-types.js.map +1 -0
- package/dist/v2.d.ts +24 -0
- package/dist/v2.d.ts.map +1 -0
- package/dist/v2.js +151 -0
- package/dist/v2.js.map +1 -0
- package/index.js +3 -0
- package/package.json +4 -3
- package/resources/threats/credentials.yaml +1 -1
- package/resources/threats/dummy.yaml +37 -4
- package/resources/threats/prompt-injection.yaml +13 -0
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";
|
|
@@ -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,
|
|
4399
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "config.json");
|
|
4354
4400
|
}
|
|
4355
4401
|
function defaultCachePath() {
|
|
4356
|
-
return (0,
|
|
4402
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
|
|
4357
4403
|
}
|
|
4358
4404
|
function defaultExceptionsPath() {
|
|
4359
|
-
return (0,
|
|
4405
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "exceptions.json");
|
|
4360
4406
|
}
|
|
4361
4407
|
function defaultAuditPath() {
|
|
4362
|
-
return (0,
|
|
4408
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "audit.jsonl");
|
|
4363
4409
|
}
|
|
4364
4410
|
function defaultOperationalLogPath() {
|
|
4365
|
-
return (0,
|
|
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,
|
|
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,
|
|
4421
|
+
const rel = (0, import_node_path2.relative)(baseDir, targetPath);
|
|
4376
4422
|
if (rel === "")
|
|
4377
4423
|
return true;
|
|
4378
|
-
if ((0,
|
|
4424
|
+
if ((0, import_node_path2.isAbsolute)(rel))
|
|
4379
4425
|
return false;
|
|
4380
|
-
return rel !== ".." && !rel.startsWith(`..${
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
20884
|
+
return "0.15.0";
|
|
20794
20885
|
try {
|
|
20795
|
-
const pkgPath = (0,
|
|
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
|
|
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,
|
|
20983
|
-
const filePath = (0,
|
|
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
|
|
21169
|
+
var import_node_crypto2 = require("node:crypto");
|
|
21082
21170
|
var import_promises4 = require("node:fs/promises");
|
|
21083
|
-
var
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
31618
|
-
const entries = await getRecentEntries(config2.logging,
|
|
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: "
|
|
31623
|
-
return textResult("No
|
|
31624
|
-
}
|
|
31625
|
-
const
|
|
31626
|
-
const
|
|
31627
|
-
|
|
31628
|
-
|
|
31629
|
-
|
|
31630
|
-
|
|
31631
|
-
|
|
31632
|
-
|
|
31633
|
-
|
|
31634
|
-
|
|
31635
|
-
|
|
31636
|
-
|
|
31637
|
-
|
|
31638
|
-
|
|
31639
|
-
|
|
31640
|
-
|
|
31641
|
-
|
|
31642
|
-
|
|
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
|
|
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
|
|
31664
|
-
2. ALWAYS pass the \`entry_ids\` parameter with just the relevant
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32012
|
+
entryIdsCount: entry_ids.length,
|
|
31707
32013
|
maxEntries: MAX_EXPLICIT_ENTRIES
|
|
31708
32014
|
});
|
|
31709
|
-
return textResult(`Too many entry_ids (${
|
|
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
|
-
|
|
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
|
|
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
|
|
31757
|
-
const
|
|
31758
|
-
const
|
|
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(
|
|
31761
|
-
const 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(
|
|
31765
|
-
const contentSnippet = readContentSnippet(
|
|
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,
|
|
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
|
-
|
|
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.
|
|
32227
|
+
version: "0.15.0",
|
|
31901
32228
|
logger,
|
|
31902
32229
|
branding
|
|
31903
32230
|
});
|