@gendigital/sage-openclaw 0.14.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/dist/index.cjs +870 -282
- package/dist/index.cjs.map +4 -4
- package/dist/mcp-server.cjs +42 -10
- package/dist/mcp-server.cjs.map +4 -4
- package/dist/model-download-worker.cjs +1 -1
- package/dist/model-download-worker.cjs.map +2 -2
- package/dist/skill-upload-worker.cjs +1 -1
- package/dist/skill-upload-worker.cjs.map +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -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/index.cjs
CHANGED
|
@@ -63,7 +63,7 @@ async function renameWithRetry(from, to, attempts = 5) {
|
|
|
63
63
|
const transient = code === "EPERM" || code === "EACCES" || code === "EBUSY";
|
|
64
64
|
if (!transient || i === attempts - 1)
|
|
65
65
|
throw err;
|
|
66
|
-
await new Promise((
|
|
66
|
+
await new Promise((resolve11) => setTimeout(resolve11, 10 * 2 ** i));
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -4292,7 +4292,10 @@ var init_zod = __esm({
|
|
|
4292
4292
|
});
|
|
4293
4293
|
|
|
4294
4294
|
// ../core/dist/types.js
|
|
4295
|
-
|
|
4295
|
+
function isSubmitOnlyThreat(threat) {
|
|
4296
|
+
return threat.flags?.includes(SUBMIT_ONLY_FLAG) ?? false;
|
|
4297
|
+
}
|
|
4298
|
+
var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, SUBMIT_ONLY_FLAG, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, SkillCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, DEFAULT_PI_TELEMETRY_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
|
|
4296
4299
|
var init_types2 = __esm({
|
|
4297
4300
|
"../core/dist/types.js"() {
|
|
4298
4301
|
"use strict";
|
|
@@ -4328,6 +4331,7 @@ var init_types2 = __esm({
|
|
|
4328
4331
|
revoked: external_exports.boolean().optional().default(false),
|
|
4329
4332
|
flags: external_exports.array(external_exports.string()).optional().default([])
|
|
4330
4333
|
});
|
|
4334
|
+
SUBMIT_ONLY_FLAG = "submit_only";
|
|
4331
4335
|
DecisionSchema = external_exports.enum(["allow", "deny", "ask"]);
|
|
4332
4336
|
SensitivitySchema = external_exports.enum(["paranoid", "balanced", "relaxed"]);
|
|
4333
4337
|
UrlCheckConfigSchema = external_exports.object({
|
|
@@ -6891,7 +6895,7 @@ async function ensurePiDeps(modelPath, logger = nullLogger) {
|
|
|
6891
6895
|
}
|
|
6892
6896
|
}
|
|
6893
6897
|
function runNpm(cwd, args, logger) {
|
|
6894
|
-
return new Promise((
|
|
6898
|
+
return new Promise((resolve11, reject) => {
|
|
6895
6899
|
const isWindows = process.platform === "win32";
|
|
6896
6900
|
const npmCmd = isWindows ? "npm.cmd" : "npm";
|
|
6897
6901
|
const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
|
|
@@ -6899,7 +6903,7 @@ function runNpm(cwd, args, logger) {
|
|
|
6899
6903
|
logger.warn(`npm install error: ${stderr || error.message}`);
|
|
6900
6904
|
reject(error);
|
|
6901
6905
|
} else {
|
|
6902
|
-
|
|
6906
|
+
resolve11();
|
|
6903
6907
|
}
|
|
6904
6908
|
});
|
|
6905
6909
|
child.unref();
|
|
@@ -14323,7 +14327,7 @@ var ApprovalStore = class {
|
|
|
14323
14327
|
};
|
|
14324
14328
|
|
|
14325
14329
|
// ../core/dist/audit-log.js
|
|
14326
|
-
var
|
|
14330
|
+
var import_node_crypto4 = require("node:crypto");
|
|
14327
14331
|
init_config();
|
|
14328
14332
|
|
|
14329
14333
|
// ../core/dist/content-snapshot.js
|
|
@@ -14501,6 +14505,47 @@ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {})
|
|
|
14501
14505
|
}
|
|
14502
14506
|
|
|
14503
14507
|
// ../core/dist/detection-names.js
|
|
14508
|
+
var import_node_crypto3 = require("node:crypto");
|
|
14509
|
+
|
|
14510
|
+
// ../core/dist/model-storage.js
|
|
14511
|
+
var import_node_fs = require("node:fs");
|
|
14512
|
+
var import_node_path4 = require("node:path");
|
|
14513
|
+
init_config();
|
|
14514
|
+
var MODEL_SCHEMA_VERSION = "v2";
|
|
14515
|
+
var REQUIRED_MODELS_BY_SCHEMA = {
|
|
14516
|
+
v1: ["pi-model"],
|
|
14517
|
+
v2: ["pi-model"]
|
|
14518
|
+
};
|
|
14519
|
+
var REQUIRED_MODEL_FILES = {
|
|
14520
|
+
"pi-model": [
|
|
14521
|
+
"model_int8.onnx",
|
|
14522
|
+
"tokenizer.json",
|
|
14523
|
+
"tokenizer_config.json",
|
|
14524
|
+
"special_tokens_map.json",
|
|
14525
|
+
"vocab.txt",
|
|
14526
|
+
"config.json"
|
|
14527
|
+
]
|
|
14528
|
+
};
|
|
14529
|
+
function getModelStorageRoot(sageDir = resolvePath("~/.sage")) {
|
|
14530
|
+
return (0, import_node_path4.join)(resolvePath(sageDir), "models");
|
|
14531
|
+
}
|
|
14532
|
+
function getModelDir(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
14533
|
+
return (0, import_node_path4.join)(getModelStorageRoot(sageDir), schema, modelName);
|
|
14534
|
+
}
|
|
14535
|
+
function isModelPresent(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
14536
|
+
const dir = getModelDir(modelName, schema, sageDir);
|
|
14537
|
+
const files = REQUIRED_MODEL_FILES[modelName];
|
|
14538
|
+
if (!files || files.length === 0)
|
|
14539
|
+
return false;
|
|
14540
|
+
return files.every((f) => (0, import_node_fs.existsSync)((0, import_node_path4.resolve)(dir, f)));
|
|
14541
|
+
}
|
|
14542
|
+
function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
14543
|
+
const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
|
|
14544
|
+
return required.filter((name) => !isModelPresent(name, schema, sageDir));
|
|
14545
|
+
}
|
|
14546
|
+
|
|
14547
|
+
// ../core/dist/detection-names.js
|
|
14548
|
+
init_types2();
|
|
14504
14549
|
var PI_DETECTION_NAME = "Other:SagePromptInjectionML-A [Susp]";
|
|
14505
14550
|
var REPORTING_ENGINE_SHORTHANDS = {
|
|
14506
14551
|
package: "sgpk",
|
|
@@ -14514,6 +14559,18 @@ function formatReportingDetectionName(canonicalName, engine, optionalData = "")
|
|
|
14514
14559
|
const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
|
|
14515
14560
|
return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
|
|
14516
14561
|
}
|
|
14562
|
+
function sha256Hex(text) {
|
|
14563
|
+
return (0, import_node_crypto3.createHash)("sha256").update(text).digest("hex");
|
|
14564
|
+
}
|
|
14565
|
+
function formatOnlineClassifierDetectionName(postfix, modelVersion, textHash) {
|
|
14566
|
+
return `Text:OnlineClassifier [${postfix}]|${modelVersion}-${textHash}|sgpi`;
|
|
14567
|
+
}
|
|
14568
|
+
function piReportingDetectionName(r) {
|
|
14569
|
+
if (r.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && r.modelVersion && r.contentSnippet) {
|
|
14570
|
+
return formatOnlineClassifierDetectionName("Misc", r.modelVersion, sha256Hex(r.contentSnippet));
|
|
14571
|
+
}
|
|
14572
|
+
return formatReportingDetectionName(PI_DETECTION_NAME, "ml", `${r.modelId}:${MODEL_SCHEMA_VERSION}`);
|
|
14573
|
+
}
|
|
14517
14574
|
var PACKAGE_DETECTION_NAMES = {
|
|
14518
14575
|
not_found: "Other:SagePackageNotFound-A [Susp]",
|
|
14519
14576
|
suspicious_age: "Other:SagePackageNew-A [Susp]",
|
|
@@ -14583,7 +14640,7 @@ init_file_utils();
|
|
|
14583
14640
|
|
|
14584
14641
|
// ../core/dist/jsonl-log-writer.js
|
|
14585
14642
|
var import_promises2 = require("node:fs/promises");
|
|
14586
|
-
var
|
|
14643
|
+
var import_node_path5 = require("node:path");
|
|
14587
14644
|
init_config();
|
|
14588
14645
|
init_file_utils();
|
|
14589
14646
|
var writeQueues = /* @__PURE__ */ new Map();
|
|
@@ -14620,7 +14677,7 @@ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
|
|
|
14620
14677
|
});
|
|
14621
14678
|
}
|
|
14622
14679
|
async function appendJsonlEntryNow(path, config2, entry) {
|
|
14623
|
-
await (0, import_promises2.mkdir)((0,
|
|
14680
|
+
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
14624
14681
|
await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
|
|
14625
14682
|
await (0, import_promises2.appendFile)(path, `${JSON.stringify(entry)}
|
|
14626
14683
|
`);
|
|
@@ -14668,7 +14725,7 @@ async function logVerdict(config2, input) {
|
|
|
14668
14725
|
return;
|
|
14669
14726
|
const entry = {
|
|
14670
14727
|
type: "runtime_verdict",
|
|
14671
|
-
entry_id: input.eventId ?? (0,
|
|
14728
|
+
entry_id: input.eventId ?? (0, import_node_crypto4.randomUUID)(),
|
|
14672
14729
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14673
14730
|
session_id: input.sessionId,
|
|
14674
14731
|
conversation_id: input.conversationId ?? input.sessionId,
|
|
@@ -14682,6 +14739,8 @@ async function logVerdict(config2, input) {
|
|
|
14682
14739
|
reasons: input.verdict.reasons,
|
|
14683
14740
|
source: input.verdict.source,
|
|
14684
14741
|
threat_id: input.verdict.matchedThreatId,
|
|
14742
|
+
...input.verdict.customThreatMatched ? { custom_threat_matched: true } : {},
|
|
14743
|
+
...input.nonCustomThreatMatched ? { non_custom_detection_matched: true } : {},
|
|
14685
14744
|
user_override: userOverride,
|
|
14686
14745
|
signals: input.signals,
|
|
14687
14746
|
tool_use_id: input.toolUseId
|
|
@@ -14734,7 +14793,7 @@ async function logSkillVerdict(config2, skillId, status, verdict, summary, conte
|
|
|
14734
14793
|
return false;
|
|
14735
14794
|
const entry = {
|
|
14736
14795
|
type: "skill_verdict",
|
|
14737
|
-
entry_id: context.entryId ?? (0,
|
|
14796
|
+
entry_id: context.entryId ?? (0, import_node_crypto4.randomUUID)(),
|
|
14738
14797
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14739
14798
|
skill_id: skillId,
|
|
14740
14799
|
status
|
|
@@ -15198,19 +15257,19 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
15198
15257
|
return this.available;
|
|
15199
15258
|
}
|
|
15200
15259
|
waitForLine(timeout) {
|
|
15201
|
-
return new Promise((
|
|
15260
|
+
return new Promise((resolve11, reject) => {
|
|
15202
15261
|
const timer = setTimeout(() => {
|
|
15203
15262
|
this.pendingResponse = null;
|
|
15204
15263
|
reject(new Error("timeout"));
|
|
15205
15264
|
}, timeout);
|
|
15206
|
-
this.pendingResponse = { resolve:
|
|
15265
|
+
this.pendingResponse = { resolve: resolve11, reject, timer };
|
|
15207
15266
|
});
|
|
15208
15267
|
}
|
|
15209
15268
|
async enqueueScan(operation) {
|
|
15210
15269
|
const previous = this.scanQueue;
|
|
15211
15270
|
let release;
|
|
15212
|
-
this.scanQueue = new Promise((
|
|
15213
|
-
release =
|
|
15271
|
+
this.scanQueue = new Promise((resolve11) => {
|
|
15272
|
+
release = resolve11;
|
|
15214
15273
|
});
|
|
15215
15274
|
await previous;
|
|
15216
15275
|
try {
|
|
@@ -15258,10 +15317,10 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
15258
15317
|
const line = this.stdoutBuffer.slice(0, idx).trim();
|
|
15259
15318
|
this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
|
|
15260
15319
|
if (this.pendingResponse) {
|
|
15261
|
-
const { resolve:
|
|
15320
|
+
const { resolve: resolve11, timer } = this.pendingResponse;
|
|
15262
15321
|
this.pendingResponse = null;
|
|
15263
15322
|
clearTimeout(timer);
|
|
15264
|
-
|
|
15323
|
+
resolve11(line);
|
|
15265
15324
|
}
|
|
15266
15325
|
idx = this.stdoutBuffer.indexOf("\n");
|
|
15267
15326
|
}
|
|
@@ -15360,7 +15419,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15360
15419
|
if (!this.available)
|
|
15361
15420
|
return null;
|
|
15362
15421
|
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
15363
|
-
return new Promise((
|
|
15422
|
+
return new Promise((resolve11) => {
|
|
15364
15423
|
try {
|
|
15365
15424
|
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
|
|
15366
15425
|
let stdout = "";
|
|
@@ -15370,7 +15429,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15370
15429
|
ps.kill();
|
|
15371
15430
|
} catch {
|
|
15372
15431
|
}
|
|
15373
|
-
|
|
15432
|
+
resolve11(null);
|
|
15374
15433
|
}, PS_TIMEOUT);
|
|
15375
15434
|
ps.stdout?.on("data", (chunk) => {
|
|
15376
15435
|
stdout += chunk.toString();
|
|
@@ -15383,7 +15442,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15383
15442
|
ps.on("error", (err) => {
|
|
15384
15443
|
clearTimeout(timer);
|
|
15385
15444
|
this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
|
|
15386
|
-
|
|
15445
|
+
resolve11(null);
|
|
15387
15446
|
});
|
|
15388
15447
|
ps.on("exit", () => {
|
|
15389
15448
|
clearTimeout(timer);
|
|
@@ -15396,11 +15455,11 @@ var WslPowershellAmsiBackend = class {
|
|
|
15396
15455
|
contentName
|
|
15397
15456
|
});
|
|
15398
15457
|
}
|
|
15399
|
-
|
|
15458
|
+
resolve11(null);
|
|
15400
15459
|
return;
|
|
15401
15460
|
}
|
|
15402
15461
|
this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
|
|
15403
|
-
|
|
15462
|
+
resolve11(interpretAmsiResult(amsiResult, content, contentName));
|
|
15404
15463
|
});
|
|
15405
15464
|
const req = JSON.stringify({ content: truncated, contentName });
|
|
15406
15465
|
ps.stdin?.write(`${req}
|
|
@@ -15408,7 +15467,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15408
15467
|
ps.stdin?.end();
|
|
15409
15468
|
} catch (e) {
|
|
15410
15469
|
this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
|
|
15411
|
-
|
|
15470
|
+
resolve11(null);
|
|
15412
15471
|
}
|
|
15413
15472
|
});
|
|
15414
15473
|
}
|
|
@@ -15548,15 +15607,15 @@ var ContentFetchClient = class _ContentFetchClient {
|
|
|
15548
15607
|
init_types2();
|
|
15549
15608
|
|
|
15550
15609
|
// ../core/dist/version.js
|
|
15551
|
-
var
|
|
15610
|
+
var import_node_path6 = require("node:path");
|
|
15552
15611
|
var import_node_url = require("node:url");
|
|
15553
15612
|
init_file_utils();
|
|
15554
15613
|
var import_meta = {};
|
|
15555
15614
|
function resolveVersion() {
|
|
15556
15615
|
if (true)
|
|
15557
|
-
return "0.
|
|
15616
|
+
return "0.15.0";
|
|
15558
15617
|
try {
|
|
15559
|
-
const pkgPath = (0,
|
|
15618
|
+
const pkgPath = (0, import_node_path6.join)((0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
|
|
15560
15619
|
const pkg = JSON.parse(getFileContentSync(pkgPath));
|
|
15561
15620
|
if (typeof pkg.version === "string")
|
|
15562
15621
|
return pkg.version;
|
|
@@ -15699,43 +15758,6 @@ var FileCheckClient = class {
|
|
|
15699
15758
|
}
|
|
15700
15759
|
};
|
|
15701
15760
|
|
|
15702
|
-
// ../core/dist/model-storage.js
|
|
15703
|
-
var import_node_fs = require("node:fs");
|
|
15704
|
-
var import_node_path6 = require("node:path");
|
|
15705
|
-
init_config();
|
|
15706
|
-
var MODEL_SCHEMA_VERSION = "v2";
|
|
15707
|
-
var REQUIRED_MODELS_BY_SCHEMA = {
|
|
15708
|
-
v1: ["pi-model"],
|
|
15709
|
-
v2: ["pi-model"]
|
|
15710
|
-
};
|
|
15711
|
-
var REQUIRED_MODEL_FILES = {
|
|
15712
|
-
"pi-model": [
|
|
15713
|
-
"model_int8.onnx",
|
|
15714
|
-
"tokenizer.json",
|
|
15715
|
-
"tokenizer_config.json",
|
|
15716
|
-
"special_tokens_map.json",
|
|
15717
|
-
"vocab.txt",
|
|
15718
|
-
"config.json"
|
|
15719
|
-
]
|
|
15720
|
-
};
|
|
15721
|
-
function getModelStorageRoot(sageDir = resolvePath("~/.sage")) {
|
|
15722
|
-
return (0, import_node_path6.join)(resolvePath(sageDir), "models");
|
|
15723
|
-
}
|
|
15724
|
-
function getModelDir(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
15725
|
-
return (0, import_node_path6.join)(getModelStorageRoot(sageDir), schema, modelName);
|
|
15726
|
-
}
|
|
15727
|
-
function isModelPresent(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
15728
|
-
const dir = getModelDir(modelName, schema, sageDir);
|
|
15729
|
-
const files = REQUIRED_MODEL_FILES[modelName];
|
|
15730
|
-
if (!files || files.length === 0)
|
|
15731
|
-
return false;
|
|
15732
|
-
return files.every((f) => (0, import_node_fs.existsSync)((0, import_node_path6.resolve)(dir, f)));
|
|
15733
|
-
}
|
|
15734
|
-
function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
15735
|
-
const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
|
|
15736
|
-
return required.filter((name) => !isModelPresent(name, schema, sageDir));
|
|
15737
|
-
}
|
|
15738
|
-
|
|
15739
15761
|
// ../core/dist/clients/model-downloader.js
|
|
15740
15762
|
init_types2();
|
|
15741
15763
|
var STALE_LOCK_MS = 60 * 60 * 1e3;
|
|
@@ -16076,6 +16098,8 @@ var RegistryClient = class {
|
|
|
16076
16098
|
};
|
|
16077
16099
|
|
|
16078
16100
|
// ../core/dist/clients/pi-check.js
|
|
16101
|
+
var import_node_crypto5 = require("node:crypto");
|
|
16102
|
+
var import_promises3 = require("node:fs/promises");
|
|
16079
16103
|
var import_node_path9 = require("node:path");
|
|
16080
16104
|
init_types2();
|
|
16081
16105
|
var DEFAULT_MAX_CONTENT_LENGTH2 = 16384;
|
|
@@ -16090,6 +16114,18 @@ var MODEL_METADATA_FILES = [
|
|
|
16090
16114
|
"vocab.txt",
|
|
16091
16115
|
MODEL_FILE
|
|
16092
16116
|
];
|
|
16117
|
+
async function computeModelVersion(modelPath, logger = nullLogger) {
|
|
16118
|
+
try {
|
|
16119
|
+
const hash = (0, import_node_crypto5.createHash)("sha256");
|
|
16120
|
+
for (const name of MODEL_METADATA_FILES) {
|
|
16121
|
+
hash.update(await (0, import_promises3.readFile)((0, import_node_path9.resolve)(modelPath, name)));
|
|
16122
|
+
}
|
|
16123
|
+
return hash.digest("hex").slice(0, 8);
|
|
16124
|
+
} catch (err) {
|
|
16125
|
+
logger.warn(`PI model version unavailable; hits keep the model-id detection name: ${err instanceof Error ? err.message : String(err)}`);
|
|
16126
|
+
return void 0;
|
|
16127
|
+
}
|
|
16128
|
+
}
|
|
16093
16129
|
function softmax(logits) {
|
|
16094
16130
|
const max = Math.max(...logits);
|
|
16095
16131
|
const exp = logits.map((x) => Math.exp(x - max));
|
|
@@ -16114,6 +16150,7 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
16114
16150
|
logger;
|
|
16115
16151
|
tokenizer = null;
|
|
16116
16152
|
session = null;
|
|
16153
|
+
modelVersion;
|
|
16117
16154
|
tensorClass = null;
|
|
16118
16155
|
inputNames = /* @__PURE__ */ new Set();
|
|
16119
16156
|
initPromise = null;
|
|
@@ -16179,6 +16216,7 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
16179
16216
|
findings,
|
|
16180
16217
|
contentName: context,
|
|
16181
16218
|
modelId,
|
|
16219
|
+
modelVersion: this.modelVersion,
|
|
16182
16220
|
contentSnippet: maxChunk || void 0
|
|
16183
16221
|
};
|
|
16184
16222
|
this.logger.info("PI model inference completed", {
|
|
@@ -16221,6 +16259,7 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
16221
16259
|
this.session = await runtime.InferenceSession.create(modelFilePath);
|
|
16222
16260
|
const sess = this.session;
|
|
16223
16261
|
this.inputNames = new Set(sess.inputNames);
|
|
16262
|
+
this.modelVersion = await computeModelVersion(this.modelPath, this.logger);
|
|
16224
16263
|
_BundledPiProvider.modelRuntimeLoaded = true;
|
|
16225
16264
|
this.logger.info("PI model loaded", { path: this.modelPath });
|
|
16226
16265
|
return true;
|
|
@@ -16423,7 +16462,7 @@ var SkillCheckClient = class {
|
|
|
16423
16462
|
init_config();
|
|
16424
16463
|
|
|
16425
16464
|
// ../core/dist/config-defaults.js
|
|
16426
|
-
var
|
|
16465
|
+
var import_promises4 = require("node:fs/promises");
|
|
16427
16466
|
var import_node_path10 = require("node:path");
|
|
16428
16467
|
init_file_utils();
|
|
16429
16468
|
init_types2();
|
|
@@ -16454,11 +16493,11 @@ async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
|
|
|
16454
16493
|
const targetPath = (0, import_node_path10.join)(sageDirPath, CONFIG_DEFAULTS_FILENAME);
|
|
16455
16494
|
const defaults = serializeConfigDefaults();
|
|
16456
16495
|
try {
|
|
16457
|
-
await (0,
|
|
16458
|
-
await (0,
|
|
16496
|
+
await (0, import_promises4.mkdir)(sageDirPath, { recursive: true, mode: 448 });
|
|
16497
|
+
await (0, import_promises4.chmod)(sageDirPath, 448);
|
|
16459
16498
|
let existing = null;
|
|
16460
16499
|
try {
|
|
16461
|
-
existing = await (0,
|
|
16500
|
+
existing = await (0, import_promises4.readFile)(targetPath, "utf-8");
|
|
16462
16501
|
} catch {
|
|
16463
16502
|
}
|
|
16464
16503
|
if (existing) {
|
|
@@ -16475,7 +16514,7 @@ async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
|
|
|
16475
16514
|
return;
|
|
16476
16515
|
}
|
|
16477
16516
|
await withFileLock(targetPath, async () => {
|
|
16478
|
-
await (0,
|
|
16517
|
+
await (0, import_promises4.writeFile)(targetPath, defaults, { encoding: "utf-8", mode: 384 });
|
|
16479
16518
|
logger.debug("Deployed config defaults", {
|
|
16480
16519
|
path: targetPath,
|
|
16481
16520
|
schemaVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
|
|
@@ -16623,7 +16662,7 @@ function isScannableContent(content) {
|
|
|
16623
16662
|
}
|
|
16624
16663
|
|
|
16625
16664
|
// ../core/dist/extended-info.js
|
|
16626
|
-
var
|
|
16665
|
+
var import_promises5 = require("node:fs/promises");
|
|
16627
16666
|
var import_node_os3 = require("node:os");
|
|
16628
16667
|
var import_node_path11 = require("node:path");
|
|
16629
16668
|
init_file_utils();
|
|
@@ -16703,7 +16742,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
|
16703
16742
|
async function loadExtendedInfoUncached(filePath, logger) {
|
|
16704
16743
|
let size;
|
|
16705
16744
|
try {
|
|
16706
|
-
const info = await (0,
|
|
16745
|
+
const info = await (0, import_promises5.stat)(filePath);
|
|
16707
16746
|
if (!info.isFile()) {
|
|
16708
16747
|
logger.debug(`extended-info: not a regular file at ${filePath}`);
|
|
16709
16748
|
return null;
|
|
@@ -16790,8 +16829,8 @@ function mergeExtendedInfo(envelope, extendedInfo) {
|
|
|
16790
16829
|
}
|
|
16791
16830
|
|
|
16792
16831
|
// ../core/dist/installation-id.js
|
|
16793
|
-
var
|
|
16794
|
-
var
|
|
16832
|
+
var import_node_crypto6 = require("node:crypto");
|
|
16833
|
+
var import_promises6 = require("node:fs/promises");
|
|
16795
16834
|
var import_node_path12 = require("node:path");
|
|
16796
16835
|
init_config();
|
|
16797
16836
|
init_file_utils();
|
|
@@ -16808,9 +16847,9 @@ async function getInstallationId(sageDirPath) {
|
|
|
16808
16847
|
} catch {
|
|
16809
16848
|
}
|
|
16810
16849
|
try {
|
|
16811
|
-
const id = (0,
|
|
16812
|
-
await (0,
|
|
16813
|
-
await (0,
|
|
16850
|
+
const id = (0, import_node_crypto6.randomUUID)();
|
|
16851
|
+
await (0, import_promises6.mkdir)(sageDir, { recursive: true, mode: 448 });
|
|
16852
|
+
await (0, import_promises6.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
|
|
16814
16853
|
return id;
|
|
16815
16854
|
} catch (err) {
|
|
16816
16855
|
if (err.code === "EEXIST") {
|
|
@@ -16994,6 +17033,10 @@ var DecisionEngine = class {
|
|
|
16994
17033
|
this.askThreshold = policy.askThreshold;
|
|
16995
17034
|
this.logger = logger;
|
|
16996
17035
|
}
|
|
17036
|
+
/** Whether these sources contain a decision signal independent of custom rules. */
|
|
17037
|
+
hasNonCustomDetection(sources) {
|
|
17038
|
+
return this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds).some((signal) => signal.customThreat !== true);
|
|
17039
|
+
}
|
|
16997
17040
|
async decide(sources) {
|
|
16998
17041
|
const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds);
|
|
16999
17042
|
if (signals.length === 0) {
|
|
@@ -17012,12 +17055,15 @@ var DecisionEngine = class {
|
|
|
17012
17055
|
source: top.source,
|
|
17013
17056
|
artifacts: allArtifacts,
|
|
17014
17057
|
matchedThreatId: top.threatId,
|
|
17058
|
+
customThreat: top.customThreat,
|
|
17015
17059
|
reasons: allReasons
|
|
17016
17060
|
};
|
|
17017
17061
|
}
|
|
17018
17062
|
collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults, piCheckResults, piThresholds) {
|
|
17019
17063
|
const signals = [];
|
|
17020
17064
|
for (const match of heuristicMatches) {
|
|
17065
|
+
if (isSubmitOnlyThreat(match.threat))
|
|
17066
|
+
continue;
|
|
17021
17067
|
const { confidence, category, severity, id, title } = match.threat;
|
|
17022
17068
|
if (applyPolicy(confidence, this.denyThreshold, this.askThreshold, this.logger) === "allow")
|
|
17023
17069
|
continue;
|
|
@@ -17027,6 +17073,7 @@ var DecisionEngine = class {
|
|
|
17027
17073
|
severity,
|
|
17028
17074
|
source: "heuristic",
|
|
17029
17075
|
threatId: id,
|
|
17076
|
+
customThreat: match.threat.customRule === true,
|
|
17030
17077
|
reason: title,
|
|
17031
17078
|
artifact: match.artifact
|
|
17032
17079
|
});
|
|
@@ -17152,17 +17199,396 @@ var DecisionEngine = class {
|
|
|
17152
17199
|
};
|
|
17153
17200
|
|
|
17154
17201
|
// ../core/dist/evaluator.js
|
|
17155
|
-
var
|
|
17202
|
+
var import_node_crypto8 = require("node:crypto");
|
|
17156
17203
|
init_config();
|
|
17157
17204
|
|
|
17205
|
+
// ../core/dist/extractors.js
|
|
17206
|
+
var URL_PATTERN = /https?:\/\/[^\s"')<>[\]{}]+/g;
|
|
17207
|
+
var TRAILING_PUNCT = /[.,;:!?]+$/;
|
|
17208
|
+
var MAX_CONTENT_SIZE = 64 * 1024;
|
|
17209
|
+
var BASE64_DECODE_EXEC = /\b(echo|printf|cat)\b[^|]*\|\s*base64\s+(-d|--decode)[^|]*\|\s*(bash|sh|zsh|ksh|dash|python|perl|ruby|node)\b/;
|
|
17210
|
+
var PRINTF_ENCODE_EXEC = /\bprintf\b\s+['"](\\x[0-9a-fA-F]{2}|\\[0-7]{3})[^|]*\|\s*(bash|sh|zsh|ksh|dash)\b/;
|
|
17211
|
+
var VAR_INTERPOLATION_URL = /\b(curl|wget|fetch)\b[^|;]*(\$\{?\w+\}?\.\$\{?\w+\}?|\$\{?\w+\}?:\/\/)/;
|
|
17212
|
+
var SHELLS = "bash|sh|zsh|ksh|dash";
|
|
17213
|
+
function extractUrls(text) {
|
|
17214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17215
|
+
const urls = [];
|
|
17216
|
+
for (const match of text.matchAll(URL_PATTERN)) {
|
|
17217
|
+
const url = match[0].replace(TRAILING_PUNCT, "");
|
|
17218
|
+
if (!seen.has(url)) {
|
|
17219
|
+
seen.add(url);
|
|
17220
|
+
urls.push(url);
|
|
17221
|
+
}
|
|
17222
|
+
}
|
|
17223
|
+
return urls;
|
|
17224
|
+
}
|
|
17225
|
+
function escapeRegExp(str) {
|
|
17226
|
+
const regExpEscape = RegExp.escape;
|
|
17227
|
+
if (regExpEscape) {
|
|
17228
|
+
return regExpEscape(str);
|
|
17229
|
+
}
|
|
17230
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17231
|
+
}
|
|
17232
|
+
function stripHeredocs(command) {
|
|
17233
|
+
return command.replace(/<<-?\s*['"]?(\w+)['"]?\n[\s\S]*?\n[ \t]*\1\b/g, "<<$1");
|
|
17234
|
+
}
|
|
17235
|
+
function extractFromBash(command) {
|
|
17236
|
+
const artifacts = [];
|
|
17237
|
+
const effective = stripHeredocs(command);
|
|
17238
|
+
artifacts.push({ type: "command", value: effective });
|
|
17239
|
+
if (BASE64_DECODE_EXEC.test(effective)) {
|
|
17240
|
+
artifacts.push({ type: "command", value: effective, context: "base64_decode_exec" });
|
|
17241
|
+
}
|
|
17242
|
+
if (PRINTF_ENCODE_EXEC.test(effective)) {
|
|
17243
|
+
artifacts.push({ type: "command", value: effective, context: "printf_encode_exec" });
|
|
17244
|
+
}
|
|
17245
|
+
if (VAR_INTERPOLATION_URL.test(effective)) {
|
|
17246
|
+
artifacts.push({ type: "command", value: effective, context: "variable_interpolation_url" });
|
|
17247
|
+
}
|
|
17248
|
+
for (const url of extractUrls(command)) {
|
|
17249
|
+
let context;
|
|
17250
|
+
const escaped = escapeRegExp(url);
|
|
17251
|
+
const directPipe = new RegExp(escaped + String.raw`[^|]*\|\s*(${SHELLS})\b`);
|
|
17252
|
+
const wrappedPipe = new RegExp(escaped + String.raw`[^|]*\|\s*\S+\s+(\S+\s+)?(${SHELLS})\b`);
|
|
17253
|
+
const subshellExec = new RegExp(`${String.raw`(\$\(|`}\`)${escaped}[^)\`]*${String.raw`\|\s*(${SHELLS})\b`}`);
|
|
17254
|
+
if (directPipe.test(command) || wrappedPipe.test(command) || subshellExec.test(command)) {
|
|
17255
|
+
context = "piped to shell";
|
|
17256
|
+
}
|
|
17257
|
+
artifacts.push({ type: "url", value: url, context });
|
|
17258
|
+
}
|
|
17259
|
+
return artifacts;
|
|
17260
|
+
}
|
|
17261
|
+
function extractFromWebFetch(toolInput) {
|
|
17262
|
+
const url = toolInput.url;
|
|
17263
|
+
if (url && typeof url === "string") {
|
|
17264
|
+
return [{ type: "url", value: url, context: "webfetch" }];
|
|
17265
|
+
}
|
|
17266
|
+
return [];
|
|
17267
|
+
}
|
|
17268
|
+
function extractFileArtifacts(toolInput, filePathKey, contentKey, contextName, urlContextName) {
|
|
17269
|
+
const artifacts = [];
|
|
17270
|
+
const filePath = toolInput[filePathKey];
|
|
17271
|
+
if (filePath && typeof filePath === "string") {
|
|
17272
|
+
artifacts.push({ type: "file_path", value: filePath, context: contextName });
|
|
17273
|
+
}
|
|
17274
|
+
const content = toolInput[contentKey];
|
|
17275
|
+
if (content && typeof content === "string" && content.trim()) {
|
|
17276
|
+
const capped = content.slice(0, MAX_CONTENT_SIZE);
|
|
17277
|
+
artifacts.push({ type: "content", value: capped, context: contextName });
|
|
17278
|
+
for (const url of extractUrls(capped)) {
|
|
17279
|
+
artifacts.push({ type: "url", value: url, context: urlContextName });
|
|
17280
|
+
}
|
|
17281
|
+
}
|
|
17282
|
+
return artifacts;
|
|
17283
|
+
}
|
|
17284
|
+
function extractFromWrite(toolInput) {
|
|
17285
|
+
return extractFileArtifacts(toolInput, "file_path", "content", "write", "from_file_content");
|
|
17286
|
+
}
|
|
17287
|
+
function extractFromEdit(toolInput) {
|
|
17288
|
+
return extractFileArtifacts(toolInput, "file_path", "new_string", "edit", "from_edit_content");
|
|
17289
|
+
}
|
|
17290
|
+
|
|
17291
|
+
// ../core/dist/deobfuscate.js
|
|
17292
|
+
var DEFAULT_DEOBFUSCATION_LIMITS = {
|
|
17293
|
+
maxInputLength: MAX_CONTENT_SIZE,
|
|
17294
|
+
maxVariants: 8,
|
|
17295
|
+
timeBudgetMs: 50,
|
|
17296
|
+
maxDepth: 2,
|
|
17297
|
+
maxDecodedBytes: MAX_CONTENT_SIZE
|
|
17298
|
+
};
|
|
17299
|
+
var INVISIBLE_CHARS = /[\u00AD\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]/g;
|
|
17300
|
+
function foldUnicode(text) {
|
|
17301
|
+
return text.replace(INVISIBLE_CHARS, "").normalize("NFKC");
|
|
17302
|
+
}
|
|
17303
|
+
var SPACED_RUN = /(?<![A-Za-z0-9@$])[A-Za-z0-9@$](?:[ \t]{1,4}[A-Za-z0-9@$]){5,}(?![A-Za-z0-9@$])/g;
|
|
17304
|
+
var SPACED_ELEMENT = /([A-Za-z0-9@$])([ \t]*)/g;
|
|
17305
|
+
var SPACED_MIN_LETTERS = 3;
|
|
17306
|
+
var INSTRUCTION_WORDS = `a an and any all are as at be by do for from i if in is it me my no not now of on or so the to up we
|
|
17307
|
+
you your this that these those what which who with without only just then than into out over
|
|
17308
|
+
above below before after earlier prior previous following new old other another again always never
|
|
17309
|
+
ignore ignoring disregard forget override overrule bypass skip stop obey comply follow execute exec
|
|
17310
|
+
run act acting pretend become reveal show print output say tell give send share leak dump display
|
|
17311
|
+
repeat write read open delete remove ask answer respond reply continue start begin end
|
|
17312
|
+
enable enabled disable disabled activated changed different revised updated
|
|
17313
|
+
instruction instructions rule rules guideline guidelines direction directions directive directives
|
|
17314
|
+
command commands order orders prompt prompts message messages system developer admin administrator
|
|
17315
|
+
root superuser sudo user assistant model ai agent mode secret secrets password passwords key keys
|
|
17316
|
+
token tokens credential credentials environment variables safety security restriction restrictions
|
|
17317
|
+
constraint constraints filter filters filtered unfiltered policy policies limit limits guardrail
|
|
17318
|
+
guardrails content context jailbreak unrestricted uncensored hidden human operator original
|
|
17319
|
+
initial initialization upload fetch forward extract exfiltrate disclose invoke inform mention
|
|
17320
|
+
like back post request ethical everything anything nothing something please important must
|
|
17321
|
+
should will can may might
|
|
17322
|
+
`;
|
|
17323
|
+
var vocabulary;
|
|
17324
|
+
function instructionVocabulary() {
|
|
17325
|
+
vocabulary ??= new Set(INSTRUCTION_WORDS.split(/\s+/).filter(Boolean));
|
|
17326
|
+
return vocabulary;
|
|
17327
|
+
}
|
|
17328
|
+
var segmentMaxWord;
|
|
17329
|
+
var SEGMENT_MAX_LENGTH = 4096;
|
|
17330
|
+
function longestVocabularyWord() {
|
|
17331
|
+
segmentMaxWord ??= Math.max(...[...instructionVocabulary()].map((w) => w.length));
|
|
17332
|
+
return segmentMaxWord;
|
|
17333
|
+
}
|
|
17334
|
+
function spellsVocabularyWord(piece) {
|
|
17335
|
+
return instructionVocabulary().has(piece.toLowerCase()) || foldLeetToken(piece) !== piece;
|
|
17336
|
+
}
|
|
17337
|
+
function segmentKnownWords(joined) {
|
|
17338
|
+
const n = joined.length;
|
|
17339
|
+
if (n === 0 || n > SEGMENT_MAX_LENGTH)
|
|
17340
|
+
return null;
|
|
17341
|
+
const best = new Array(n + 1).fill(Number.POSITIVE_INFINITY);
|
|
17342
|
+
const prev = new Array(n + 1).fill(-1);
|
|
17343
|
+
best[0] = 0;
|
|
17344
|
+
for (let end = 1; end <= n; end++) {
|
|
17345
|
+
let bestHere = Number.POSITIVE_INFINITY;
|
|
17346
|
+
for (let len = 1; len <= Math.min(longestVocabularyWord(), end); len++) {
|
|
17347
|
+
const start = end - len;
|
|
17348
|
+
const words2 = best[start] ?? Number.POSITIVE_INFINITY;
|
|
17349
|
+
if (words2 + 1 >= bestHere || !spellsVocabularyWord(joined.slice(start, end)))
|
|
17350
|
+
continue;
|
|
17351
|
+
bestHere = words2 + 1;
|
|
17352
|
+
best[end] = bestHere;
|
|
17353
|
+
prev[end] = start;
|
|
17354
|
+
}
|
|
17355
|
+
}
|
|
17356
|
+
if ((best[n] ?? Number.POSITIVE_INFINITY) === Number.POSITIVE_INFINITY)
|
|
17357
|
+
return null;
|
|
17358
|
+
const words = [];
|
|
17359
|
+
for (let end = n; end > 0; ) {
|
|
17360
|
+
const start = prev[end] ?? 0;
|
|
17361
|
+
words.push(joined.slice(start, end));
|
|
17362
|
+
end = start;
|
|
17363
|
+
}
|
|
17364
|
+
return words.reverse().join(" ");
|
|
17365
|
+
}
|
|
17366
|
+
function collapseSpacedRun(run) {
|
|
17367
|
+
const elements = [];
|
|
17368
|
+
for (const m of run.matchAll(SPACED_ELEMENT)) {
|
|
17369
|
+
elements.push({ ch: m[1] ?? "", gap: (m[2] ?? "").length });
|
|
17370
|
+
}
|
|
17371
|
+
const letters = elements.reduce((n, e) => n + (/[A-Za-z]/.test(e.ch) ? 1 : 0), 0);
|
|
17372
|
+
if (letters < SPACED_MIN_LETTERS)
|
|
17373
|
+
return run;
|
|
17374
|
+
const gaps = elements.slice(0, -1).map((e) => e.gap);
|
|
17375
|
+
const minGap = Math.min(...gaps);
|
|
17376
|
+
const uniform = gaps.every((g) => g === minGap);
|
|
17377
|
+
let out = "";
|
|
17378
|
+
elements.forEach((e, i) => {
|
|
17379
|
+
out += e.ch;
|
|
17380
|
+
if (i < elements.length - 1 && e.gap > minGap)
|
|
17381
|
+
out += " ";
|
|
17382
|
+
});
|
|
17383
|
+
return uniform ? segmentKnownWords(out) ?? out : out;
|
|
17384
|
+
}
|
|
17385
|
+
function collapseSpaced(text) {
|
|
17386
|
+
return text.replace(SPACED_RUN, collapseSpacedRun);
|
|
17387
|
+
}
|
|
17388
|
+
var LEET_MAP = {
|
|
17389
|
+
"0": "o",
|
|
17390
|
+
"3": "e",
|
|
17391
|
+
"4": "a",
|
|
17392
|
+
"5": "s",
|
|
17393
|
+
"7": "t",
|
|
17394
|
+
"@": "a",
|
|
17395
|
+
$: "s"
|
|
17396
|
+
};
|
|
17397
|
+
var WORD_TOKEN = /[A-Za-z0-9@$]+/g;
|
|
17398
|
+
var LETTER = /[A-Za-z]/;
|
|
17399
|
+
var LEET_CHARS = /[0-9@$]/;
|
|
17400
|
+
var LEET_MIN_TOKEN = 3;
|
|
17401
|
+
var NUMBER_WITH_SUFFIX = /^[0-9]+[A-Za-z]$/;
|
|
17402
|
+
function foldLeetToken(token) {
|
|
17403
|
+
if (token.length < LEET_MIN_TOKEN || !LETTER.test(token) || !LEET_CHARS.test(token)) {
|
|
17404
|
+
return token;
|
|
17405
|
+
}
|
|
17406
|
+
if (NUMBER_WITH_SUFFIX.test(token))
|
|
17407
|
+
return token;
|
|
17408
|
+
for (const one of ["i", "l"]) {
|
|
17409
|
+
let folded = "";
|
|
17410
|
+
for (const ch of token)
|
|
17411
|
+
folded += ch === "1" ? one : LEET_MAP[ch] ?? ch;
|
|
17412
|
+
if (instructionVocabulary().has(folded.toLowerCase()))
|
|
17413
|
+
return folded;
|
|
17414
|
+
if (!token.includes("1"))
|
|
17415
|
+
break;
|
|
17416
|
+
}
|
|
17417
|
+
return token;
|
|
17418
|
+
}
|
|
17419
|
+
function foldLeet(text) {
|
|
17420
|
+
return text.replace(WORD_TOKEN, foldLeetToken);
|
|
17421
|
+
}
|
|
17422
|
+
var BASE64_CANDIDATE = /(?<![A-Za-z0-9+/_-])[A-Za-z0-9+/_-]{16,}={0,2}(?![A-Za-z0-9+/=_-])/g;
|
|
17423
|
+
var BASE64_MIN_ALNUM_RATIO = 0.85;
|
|
17424
|
+
var HEX_CONTIGUOUS = /(?<![0-9A-Za-z])(?:[0-9A-Fa-f]{2}){8,}[0-9A-Fa-f]?(?![0-9A-Za-z])/g;
|
|
17425
|
+
var HEX_ESCAPES = /(?:\\x[0-9A-Fa-f]{2}){6,}/g;
|
|
17426
|
+
var HEX_SEPARATED = /(?<![0-9A-Za-z])[0-9A-Fa-f]{2}(?:[ :,-][0-9A-Fa-f]{2}){7,}(?![0-9A-Za-z])/g;
|
|
17427
|
+
var MIN_DECODED_BYTES = 8;
|
|
17428
|
+
var MIN_PRINTABLE_RATIO = 0.95;
|
|
17429
|
+
var MIN_WORDISH_RATIO = 0.6;
|
|
17430
|
+
var WORDISH = /[\p{L}\s]/u;
|
|
17431
|
+
var REPLACEMENT_CHAR = String.fromCharCode(65533);
|
|
17432
|
+
function decodeIfText(bytes) {
|
|
17433
|
+
if (bytes.length < MIN_DECODED_BYTES)
|
|
17434
|
+
return null;
|
|
17435
|
+
const text = bytes.toString("utf8");
|
|
17436
|
+
if (text.includes(REPLACEMENT_CHAR))
|
|
17437
|
+
return null;
|
|
17438
|
+
let total = 0;
|
|
17439
|
+
let printable = 0;
|
|
17440
|
+
let wordish = 0;
|
|
17441
|
+
for (const ch of text) {
|
|
17442
|
+
total++;
|
|
17443
|
+
const c = ch.codePointAt(0) ?? 0;
|
|
17444
|
+
if (c >= 32 && c < 127 || c === 9 || c === 10 || c === 13 || c >= 160) {
|
|
17445
|
+
printable++;
|
|
17446
|
+
}
|
|
17447
|
+
if (WORDISH.test(ch))
|
|
17448
|
+
wordish++;
|
|
17449
|
+
}
|
|
17450
|
+
if (total === 0)
|
|
17451
|
+
return null;
|
|
17452
|
+
if (printable / total < MIN_PRINTABLE_RATIO)
|
|
17453
|
+
return null;
|
|
17454
|
+
if (wordish / total < MIN_WORDISH_RATIO && !isHexDump(text))
|
|
17455
|
+
return null;
|
|
17456
|
+
return text;
|
|
17457
|
+
}
|
|
17458
|
+
function isHexDump(text) {
|
|
17459
|
+
const digits = text.replace(/\\x|[\s:,-]/g, "");
|
|
17460
|
+
return digits.length >= 16 && digits.length % 2 === 0 && /^[0-9A-Fa-f]+$/.test(digits);
|
|
17461
|
+
}
|
|
17462
|
+
function alnumRatio(s) {
|
|
17463
|
+
let n = 0;
|
|
17464
|
+
for (let i = 0; i < s.length; i++) {
|
|
17465
|
+
const c = s.charCodeAt(i);
|
|
17466
|
+
if (c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122)
|
|
17467
|
+
n++;
|
|
17468
|
+
}
|
|
17469
|
+
return n / s.length;
|
|
17470
|
+
}
|
|
17471
|
+
function decodeCandidates(candidates, toBytes, budget) {
|
|
17472
|
+
const decoded = [];
|
|
17473
|
+
for (const candidate of candidates) {
|
|
17474
|
+
if (budget.bytesLeft < MIN_DECODED_BYTES)
|
|
17475
|
+
break;
|
|
17476
|
+
const bytes = toBytes(candidate);
|
|
17477
|
+
if (bytes === null || bytes.length > budget.bytesLeft)
|
|
17478
|
+
continue;
|
|
17479
|
+
const text = decodeIfText(bytes);
|
|
17480
|
+
if (text === null)
|
|
17481
|
+
continue;
|
|
17482
|
+
budget.bytesLeft -= bytes.length;
|
|
17483
|
+
decoded.push(text);
|
|
17484
|
+
}
|
|
17485
|
+
return decoded.length > 0 ? decoded.join("\n") : null;
|
|
17486
|
+
}
|
|
17487
|
+
function base64Bytes(candidate) {
|
|
17488
|
+
const token = candidate.replace(/=+$/, "");
|
|
17489
|
+
if (token.length % 4 === 1 || alnumRatio(token) < BASE64_MIN_ALNUM_RATIO)
|
|
17490
|
+
return null;
|
|
17491
|
+
if (!LETTER.test(token))
|
|
17492
|
+
return null;
|
|
17493
|
+
return Buffer.from(token.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
17494
|
+
}
|
|
17495
|
+
function hexBytes(candidate) {
|
|
17496
|
+
const digits = candidate.replace(/\\x|[^0-9A-Fa-f]/g, "");
|
|
17497
|
+
return Buffer.from(digits.length % 2 === 1 ? digits.slice(0, -1) : digits, "hex");
|
|
17498
|
+
}
|
|
17499
|
+
function decodeBase64Snippets(text, budget) {
|
|
17500
|
+
return decodeCandidates(text.matchAll(BASE64_CANDIDATE), (m) => base64Bytes(m[0]), budget);
|
|
17501
|
+
}
|
|
17502
|
+
function decodeHexSnippets(text, budget) {
|
|
17503
|
+
const matches = [HEX_CONTIGUOUS, HEX_ESCAPES, HEX_SEPARATED].flatMap((p) => [
|
|
17504
|
+
...text.matchAll(p)
|
|
17505
|
+
]);
|
|
17506
|
+
return decodeCandidates(matches, (m) => hexBytes(m[0]), budget);
|
|
17507
|
+
}
|
|
17508
|
+
function exhausted(run) {
|
|
17509
|
+
return Date.now() > run.deadline || run.variants.length >= run.limits.maxVariants;
|
|
17510
|
+
}
|
|
17511
|
+
function addVariant(run, text, techniques) {
|
|
17512
|
+
const { maxInputLength, maxVariants } = run.limits;
|
|
17513
|
+
const clamped = text.length > maxInputLength ? text.slice(0, maxInputLength) : text;
|
|
17514
|
+
if (run.seen.has(clamped) || run.variants.length >= maxVariants)
|
|
17515
|
+
return false;
|
|
17516
|
+
run.seen.add(clamped);
|
|
17517
|
+
run.variants.push({ text: clamped, techniques });
|
|
17518
|
+
return true;
|
|
17519
|
+
}
|
|
17520
|
+
function expand(run, text, depth, prefix) {
|
|
17521
|
+
let current = text;
|
|
17522
|
+
const applied = [...prefix];
|
|
17523
|
+
const folded = foldUnicode(current);
|
|
17524
|
+
if (folded !== current) {
|
|
17525
|
+
current = folded;
|
|
17526
|
+
applied.push("unicode");
|
|
17527
|
+
addVariant(run, current, [...applied]);
|
|
17528
|
+
}
|
|
17529
|
+
const decodeSource = current;
|
|
17530
|
+
if (!exhausted(run)) {
|
|
17531
|
+
const collapsed = collapseSpaced(current);
|
|
17532
|
+
if (collapsed !== current) {
|
|
17533
|
+
current = collapsed;
|
|
17534
|
+
applied.push("spaced");
|
|
17535
|
+
addVariant(run, current, [...applied]);
|
|
17536
|
+
}
|
|
17537
|
+
}
|
|
17538
|
+
if (!exhausted(run)) {
|
|
17539
|
+
const leetFolded = foldLeet(current);
|
|
17540
|
+
if (leetFolded !== current)
|
|
17541
|
+
addVariant(run, leetFolded, [...applied, "leet"]);
|
|
17542
|
+
}
|
|
17543
|
+
if (depth >= run.limits.maxDepth)
|
|
17544
|
+
return;
|
|
17545
|
+
const decoders = [
|
|
17546
|
+
["base64", decodeBase64Snippets],
|
|
17547
|
+
["hex", decodeHexSnippets]
|
|
17548
|
+
];
|
|
17549
|
+
for (const [technique, decode] of decoders) {
|
|
17550
|
+
if (exhausted(run))
|
|
17551
|
+
return;
|
|
17552
|
+
const decoded = decode(decodeSource, run.budget);
|
|
17553
|
+
if (decoded === null)
|
|
17554
|
+
continue;
|
|
17555
|
+
const techniques = [...prefix, technique];
|
|
17556
|
+
if (addVariant(run, decoded, techniques)) {
|
|
17557
|
+
expand(run, decoded, depth + 1, techniques);
|
|
17558
|
+
}
|
|
17559
|
+
}
|
|
17560
|
+
}
|
|
17561
|
+
function deobfuscate(input, limits = {}) {
|
|
17562
|
+
const resolved = { ...DEFAULT_DEOBFUSCATION_LIMITS, ...limits };
|
|
17563
|
+
const original = input.length > resolved.maxInputLength ? input.slice(0, resolved.maxInputLength) : input;
|
|
17564
|
+
const run = {
|
|
17565
|
+
limits: resolved,
|
|
17566
|
+
deadline: Date.now() + resolved.timeBudgetMs,
|
|
17567
|
+
seen: /* @__PURE__ */ new Set([original]),
|
|
17568
|
+
variants: [],
|
|
17569
|
+
budget: { bytesLeft: resolved.maxDecodedBytes }
|
|
17570
|
+
};
|
|
17571
|
+
if (!original)
|
|
17572
|
+
return run.variants;
|
|
17573
|
+
try {
|
|
17574
|
+
expand(run, original, 0, []);
|
|
17575
|
+
} catch {
|
|
17576
|
+
}
|
|
17577
|
+
return run.variants;
|
|
17578
|
+
}
|
|
17579
|
+
var DEOBFUSCATED_CONTEXT_PREFIX = "deobfuscated:";
|
|
17580
|
+
function deobfuscationContext(techniques) {
|
|
17581
|
+
return `${DEOBFUSCATED_CONTEXT_PREFIX}${techniques.join("+")}`;
|
|
17582
|
+
}
|
|
17583
|
+
|
|
17158
17584
|
// ../core/dist/exceptions.js
|
|
17159
|
-
var
|
|
17585
|
+
var import_node_crypto7 = require("node:crypto");
|
|
17160
17586
|
var import_node_path15 = require("node:path");
|
|
17161
17587
|
init_config();
|
|
17162
17588
|
init_file_utils();
|
|
17163
17589
|
|
|
17164
17590
|
// ../core/dist/trusted-domains.js
|
|
17165
|
-
var
|
|
17591
|
+
var import_promises7 = require("node:fs/promises");
|
|
17166
17592
|
var import_node_path14 = require("node:path");
|
|
17167
17593
|
var import_yaml = __toESM(require_dist(), 1);
|
|
17168
17594
|
init_file_utils();
|
|
@@ -17170,7 +17596,7 @@ init_types2();
|
|
|
17170
17596
|
async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
|
|
17171
17597
|
let files;
|
|
17172
17598
|
try {
|
|
17173
|
-
files = (await (0,
|
|
17599
|
+
files = (await (0, import_promises7.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
17174
17600
|
} catch {
|
|
17175
17601
|
logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
|
|
17176
17602
|
return [];
|
|
@@ -17242,7 +17668,7 @@ init_types2();
|
|
|
17242
17668
|
var MAX_RULES_WARNING = 100;
|
|
17243
17669
|
var REGEX_TIMEOUT_MS = 50;
|
|
17244
17670
|
function computeRuleId(decision, match, pattern) {
|
|
17245
|
-
const hash = (0,
|
|
17671
|
+
const hash = (0, import_node_crypto7.createHash)("sha256").update(`${decision}:${match}:${pattern}`).digest("hex");
|
|
17246
17672
|
return hash.slice(0, 8);
|
|
17247
17673
|
}
|
|
17248
17674
|
async function loadExceptions(config2, logger = nullLogger) {
|
|
@@ -17608,93 +18034,8 @@ function matchesArtifact(rule, artifact) {
|
|
|
17608
18034
|
}
|
|
17609
18035
|
}
|
|
17610
18036
|
|
|
17611
|
-
// ../core/dist/extractors.js
|
|
17612
|
-
var URL_PATTERN = /https?:\/\/[^\s"')<>[\]{}]+/g;
|
|
17613
|
-
var TRAILING_PUNCT = /[.,;:!?]+$/;
|
|
17614
|
-
var MAX_CONTENT_SIZE = 64 * 1024;
|
|
17615
|
-
var BASE64_DECODE_EXEC = /\b(echo|printf|cat)\b[^|]*\|\s*base64\s+(-d|--decode)[^|]*\|\s*(bash|sh|zsh|ksh|dash|python|perl|ruby|node)\b/;
|
|
17616
|
-
var PRINTF_ENCODE_EXEC = /\bprintf\b\s+['"](\\x[0-9a-fA-F]{2}|\\[0-7]{3})[^|]*\|\s*(bash|sh|zsh|ksh|dash)\b/;
|
|
17617
|
-
var VAR_INTERPOLATION_URL = /\b(curl|wget|fetch)\b[^|;]*(\$\{?\w+\}?\.\$\{?\w+\}?|\$\{?\w+\}?:\/\/)/;
|
|
17618
|
-
var SHELLS = "bash|sh|zsh|ksh|dash";
|
|
17619
|
-
function extractUrls(text) {
|
|
17620
|
-
const seen = /* @__PURE__ */ new Set();
|
|
17621
|
-
const urls = [];
|
|
17622
|
-
for (const match of text.matchAll(URL_PATTERN)) {
|
|
17623
|
-
const url = match[0].replace(TRAILING_PUNCT, "");
|
|
17624
|
-
if (!seen.has(url)) {
|
|
17625
|
-
seen.add(url);
|
|
17626
|
-
urls.push(url);
|
|
17627
|
-
}
|
|
17628
|
-
}
|
|
17629
|
-
return urls;
|
|
17630
|
-
}
|
|
17631
|
-
function escapeRegExp(str) {
|
|
17632
|
-
const regExpEscape = RegExp.escape;
|
|
17633
|
-
if (regExpEscape) {
|
|
17634
|
-
return regExpEscape(str);
|
|
17635
|
-
}
|
|
17636
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17637
|
-
}
|
|
17638
|
-
function stripHeredocs(command) {
|
|
17639
|
-
return command.replace(/<<-?\s*['"]?(\w+)['"]?\n[\s\S]*?\n[ \t]*\1\b/g, "<<$1");
|
|
17640
|
-
}
|
|
17641
|
-
function extractFromBash(command) {
|
|
17642
|
-
const artifacts = [];
|
|
17643
|
-
const effective = stripHeredocs(command);
|
|
17644
|
-
artifacts.push({ type: "command", value: effective });
|
|
17645
|
-
if (BASE64_DECODE_EXEC.test(effective)) {
|
|
17646
|
-
artifacts.push({ type: "command", value: effective, context: "base64_decode_exec" });
|
|
17647
|
-
}
|
|
17648
|
-
if (PRINTF_ENCODE_EXEC.test(effective)) {
|
|
17649
|
-
artifacts.push({ type: "command", value: effective, context: "printf_encode_exec" });
|
|
17650
|
-
}
|
|
17651
|
-
if (VAR_INTERPOLATION_URL.test(effective)) {
|
|
17652
|
-
artifacts.push({ type: "command", value: effective, context: "variable_interpolation_url" });
|
|
17653
|
-
}
|
|
17654
|
-
for (const url of extractUrls(command)) {
|
|
17655
|
-
let context;
|
|
17656
|
-
const escaped = escapeRegExp(url);
|
|
17657
|
-
const directPipe = new RegExp(escaped + String.raw`[^|]*\|\s*(${SHELLS})\b`);
|
|
17658
|
-
const wrappedPipe = new RegExp(escaped + String.raw`[^|]*\|\s*\S+\s+(\S+\s+)?(${SHELLS})\b`);
|
|
17659
|
-
const subshellExec = new RegExp(`${String.raw`(\$\(|`}\`)${escaped}[^)\`]*${String.raw`\|\s*(${SHELLS})\b`}`);
|
|
17660
|
-
if (directPipe.test(command) || wrappedPipe.test(command) || subshellExec.test(command)) {
|
|
17661
|
-
context = "piped to shell";
|
|
17662
|
-
}
|
|
17663
|
-
artifacts.push({ type: "url", value: url, context });
|
|
17664
|
-
}
|
|
17665
|
-
return artifacts;
|
|
17666
|
-
}
|
|
17667
|
-
function extractFromWebFetch(toolInput) {
|
|
17668
|
-
const url = toolInput.url;
|
|
17669
|
-
if (url && typeof url === "string") {
|
|
17670
|
-
return [{ type: "url", value: url, context: "webfetch" }];
|
|
17671
|
-
}
|
|
17672
|
-
return [];
|
|
17673
|
-
}
|
|
17674
|
-
function extractFileArtifacts(toolInput, filePathKey, contentKey, contextName, urlContextName) {
|
|
17675
|
-
const artifacts = [];
|
|
17676
|
-
const filePath = toolInput[filePathKey];
|
|
17677
|
-
if (filePath && typeof filePath === "string") {
|
|
17678
|
-
artifacts.push({ type: "file_path", value: filePath, context: contextName });
|
|
17679
|
-
}
|
|
17680
|
-
const content = toolInput[contentKey];
|
|
17681
|
-
if (content && typeof content === "string" && content.trim()) {
|
|
17682
|
-
const capped = content.slice(0, MAX_CONTENT_SIZE);
|
|
17683
|
-
artifacts.push({ type: "content", value: capped, context: contextName });
|
|
17684
|
-
for (const url of extractUrls(capped)) {
|
|
17685
|
-
artifacts.push({ type: "url", value: url, context: urlContextName });
|
|
17686
|
-
}
|
|
17687
|
-
}
|
|
17688
|
-
return artifacts;
|
|
17689
|
-
}
|
|
17690
|
-
function extractFromWrite(toolInput) {
|
|
17691
|
-
return extractFileArtifacts(toolInput, "file_path", "content", "write", "from_file_content");
|
|
17692
|
-
}
|
|
17693
|
-
function extractFromEdit(toolInput) {
|
|
17694
|
-
return extractFileArtifacts(toolInput, "file_path", "new_string", "edit", "from_edit_content");
|
|
17695
|
-
}
|
|
17696
|
-
|
|
17697
18037
|
// ../core/dist/heuristics.js
|
|
18038
|
+
var MAX_VARIANTS_PER_CALL = 16;
|
|
17698
18039
|
var TRUSTED_DOMAIN_SUPPRESSIBLE = /* @__PURE__ */ new Set([
|
|
17699
18040
|
"CLT-CMD-001",
|
|
17700
18041
|
"CLT-CMD-002",
|
|
@@ -17755,6 +18096,51 @@ var HeuristicsEngine = class {
|
|
|
17755
18096
|
}
|
|
17756
18097
|
return matches;
|
|
17757
18098
|
}
|
|
18099
|
+
/**
|
|
18100
|
+
* Match deobfuscated variants of the `content` artifacts against this
|
|
18101
|
+
* engine's prompt-injection rules only. Command, credential, URL and other
|
|
18102
|
+
* rules never see decoded or folded text, so deobfuscation adds coverage for
|
|
18103
|
+
* hidden instructions without widening any other rule's false-positive surface.
|
|
18104
|
+
*
|
|
18105
|
+
* A variant match is attributed to the *original* artifact (`artifact`), and
|
|
18106
|
+
* only the short matched snippet (`matchValue`) comes from decoded text; the
|
|
18107
|
+
* decoded variant itself is never stored or passed on.
|
|
18108
|
+
*
|
|
18109
|
+
* Each rule is reported at most once, and not at all when its ID is in
|
|
18110
|
+
* `alreadyMatched` (the raw text already tripped it), so audit and telemetry
|
|
18111
|
+
* signals are not duplicated.
|
|
18112
|
+
*/
|
|
18113
|
+
matchDeobfuscated(artifacts, alreadyMatched = /* @__PURE__ */ new Set()) {
|
|
18114
|
+
const piThreats = (this.threatMap.get("content") ?? []).filter((t) => t.category === "prompt_injection");
|
|
18115
|
+
if (piThreats.length === 0)
|
|
18116
|
+
return [];
|
|
18117
|
+
const matches = [];
|
|
18118
|
+
const reported = new Set(alreadyMatched);
|
|
18119
|
+
let variantsLeft = MAX_VARIANTS_PER_CALL;
|
|
18120
|
+
for (const artifact of artifacts) {
|
|
18121
|
+
if (artifact.type !== "content")
|
|
18122
|
+
continue;
|
|
18123
|
+
for (const variant of deobfuscate(artifact.value)) {
|
|
18124
|
+
if (variantsLeft-- <= 0)
|
|
18125
|
+
return matches;
|
|
18126
|
+
for (const threat of piThreats) {
|
|
18127
|
+
if (reported.has(threat.id))
|
|
18128
|
+
continue;
|
|
18129
|
+
const m = threat.compiledPattern.exec(variant.text);
|
|
18130
|
+
if (!m)
|
|
18131
|
+
continue;
|
|
18132
|
+
reported.add(threat.id);
|
|
18133
|
+
matches.push({
|
|
18134
|
+
threat,
|
|
18135
|
+
artifact: artifact.value,
|
|
18136
|
+
matchValue: m[0],
|
|
18137
|
+
context: deobfuscationContext(variant.techniques)
|
|
18138
|
+
});
|
|
18139
|
+
}
|
|
18140
|
+
}
|
|
18141
|
+
}
|
|
18142
|
+
return matches;
|
|
18143
|
+
}
|
|
17758
18144
|
};
|
|
17759
18145
|
|
|
17760
18146
|
// ../core/dist/package-checker.js
|
|
@@ -18294,12 +18680,98 @@ async function updateSessionStatus(sessionId, verdict) {
|
|
|
18294
18680
|
await atomicWriteJson(statusFilePath(sessionId), status);
|
|
18295
18681
|
}
|
|
18296
18682
|
|
|
18297
|
-
// ../core/dist/
|
|
18298
|
-
var
|
|
18683
|
+
// ../core/dist/submit-limits.js
|
|
18684
|
+
var import_promises8 = require("node:fs/promises");
|
|
18299
18685
|
var import_node_path17 = require("node:path");
|
|
18686
|
+
init_config();
|
|
18687
|
+
init_file_utils();
|
|
18688
|
+
init_types2();
|
|
18689
|
+
var SUBMIT_LIMITS_SCHEMA_VERSION = 1;
|
|
18690
|
+
var SUBMIT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
18691
|
+
function submitLimitsPath(sageDirPath) {
|
|
18692
|
+
return (0, import_node_path17.join)(sageDirPath ?? resolvePath(SAGE_DIR), "submit-limits.json");
|
|
18693
|
+
}
|
|
18694
|
+
function isEligible(limits, ruleId, now) {
|
|
18695
|
+
const last = limits[ruleId];
|
|
18696
|
+
if (last === void 0)
|
|
18697
|
+
return true;
|
|
18698
|
+
return now - last >= SUBMIT_WINDOW_MS || last > now;
|
|
18699
|
+
}
|
|
18700
|
+
async function loadSubmitLimits(opts = {}) {
|
|
18701
|
+
const logger = opts.logger ?? nullLogger;
|
|
18702
|
+
let raw;
|
|
18703
|
+
try {
|
|
18704
|
+
raw = await getFileContent(submitLimitsPath(opts.sageDirPath));
|
|
18705
|
+
} catch {
|
|
18706
|
+
return {};
|
|
18707
|
+
}
|
|
18708
|
+
try {
|
|
18709
|
+
const data = JSON.parse(raw);
|
|
18710
|
+
const rules = data.rules ?? {};
|
|
18711
|
+
const limits = {};
|
|
18712
|
+
for (const [id, rec] of Object.entries(rules)) {
|
|
18713
|
+
const last = rec?.last_submitted_ms;
|
|
18714
|
+
if (typeof last === "number" && Number.isFinite(last))
|
|
18715
|
+
limits[id] = last;
|
|
18716
|
+
}
|
|
18717
|
+
return limits;
|
|
18718
|
+
} catch (e) {
|
|
18719
|
+
logger.warn("Failed to parse submit limits; treating as empty", { error: String(e) });
|
|
18720
|
+
return {};
|
|
18721
|
+
}
|
|
18722
|
+
}
|
|
18723
|
+
async function saveSubmitLimits(limits, sageDirPath) {
|
|
18724
|
+
await atomicWriteJson(submitLimitsPath(sageDirPath), {
|
|
18725
|
+
schema_version: SUBMIT_LIMITS_SCHEMA_VERSION,
|
|
18726
|
+
rules: Object.fromEntries(Object.entries(limits).map(([id, last]) => [id, { last_submitted_ms: last }]))
|
|
18727
|
+
});
|
|
18728
|
+
}
|
|
18729
|
+
async function claimRuleSubmissions(ruleIds, opts = {}) {
|
|
18730
|
+
const logger = opts.logger ?? nullLogger;
|
|
18731
|
+
const unique = [...new Set(ruleIds)];
|
|
18732
|
+
if (unique.length === 0)
|
|
18733
|
+
return [];
|
|
18734
|
+
const now = opts.now ?? Date.now();
|
|
18735
|
+
const path = submitLimitsPath(opts.sageDirPath);
|
|
18736
|
+
let claimed = [];
|
|
18737
|
+
try {
|
|
18738
|
+
await (0, import_promises8.mkdir)((0, import_node_path17.dirname)(path), { recursive: true });
|
|
18739
|
+
await withFileLock(path, async () => {
|
|
18740
|
+
const limits = await loadSubmitLimits(opts);
|
|
18741
|
+
const eligible = opts.force ? unique : unique.filter((id) => isEligible(limits, id, now));
|
|
18742
|
+
if (eligible.length === 0)
|
|
18743
|
+
return;
|
|
18744
|
+
const next = {};
|
|
18745
|
+
for (const [id, last] of Object.entries(limits)) {
|
|
18746
|
+
if (isEligible(limits, id, now))
|
|
18747
|
+
continue;
|
|
18748
|
+
next[id] = last;
|
|
18749
|
+
}
|
|
18750
|
+
for (const id of eligible)
|
|
18751
|
+
next[id] = now;
|
|
18752
|
+
await saveSubmitLimits(next, opts.sageDirPath);
|
|
18753
|
+
claimed = eligible;
|
|
18754
|
+
});
|
|
18755
|
+
} catch (e) {
|
|
18756
|
+
logger.warn("Failed to persist submit limits; suppressing submission", { error: String(e) });
|
|
18757
|
+
return [];
|
|
18758
|
+
}
|
|
18759
|
+
if (claimed.length === 0)
|
|
18760
|
+
logger.debug("Rule submissions capped", { ruleIds: unique });
|
|
18761
|
+
return claimed;
|
|
18762
|
+
}
|
|
18763
|
+
|
|
18764
|
+
// ../core/dist/threat-loader.js
|
|
18765
|
+
var import_promises9 = require("node:fs/promises");
|
|
18766
|
+
var import_node_path18 = require("node:path");
|
|
18300
18767
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
18768
|
+
init_config();
|
|
18301
18769
|
init_file_utils();
|
|
18302
18770
|
init_types2();
|
|
18771
|
+
var MAX_THREAT_RULES = 1e4;
|
|
18772
|
+
function getUserThreatsDir(sageDirPath) {
|
|
18773
|
+
return (0, import_node_path18.join)(resolvePath(sageDirPath ?? SAGE_DIR), "threats");
|
|
18774
|
+
}
|
|
18303
18775
|
var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
|
|
18304
18776
|
"id",
|
|
18305
18777
|
"version",
|
|
@@ -18358,13 +18830,21 @@ function isExpired(entry) {
|
|
|
18358
18830
|
return false;
|
|
18359
18831
|
return Date.now() > expiresAt.getTime();
|
|
18360
18832
|
}
|
|
18361
|
-
async function
|
|
18833
|
+
async function loadThreatDirectory(threatDir, logger, { custom: custom2 = false, customThreatIds = /* @__PURE__ */ new Set(), bundledThreatIds = /* @__PURE__ */ new Set(), maxRules = MAX_THREAT_RULES, logCustomRuleIds = false, optional = false } = {}) {
|
|
18362
18834
|
const threats = [];
|
|
18363
18835
|
let files;
|
|
18364
18836
|
try {
|
|
18365
|
-
files = (await (0,
|
|
18366
|
-
} catch {
|
|
18367
|
-
|
|
18837
|
+
files = (await (0, import_promises9.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
18838
|
+
} catch (error) {
|
|
18839
|
+
const code = error.code;
|
|
18840
|
+
if (optional && code === "ENOENT") {
|
|
18841
|
+
logger.debug("Custom threat directory does not exist", { path: threatDir });
|
|
18842
|
+
return threats;
|
|
18843
|
+
}
|
|
18844
|
+
logger.warn("Threat directory does not exist or is unreadable", {
|
|
18845
|
+
path: threatDir,
|
|
18846
|
+
error: String(error)
|
|
18847
|
+
});
|
|
18368
18848
|
return threats;
|
|
18369
18849
|
}
|
|
18370
18850
|
const ruleFiles = [];
|
|
@@ -18372,7 +18852,7 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18372
18852
|
for (const filename of files) {
|
|
18373
18853
|
let content;
|
|
18374
18854
|
try {
|
|
18375
|
-
content = await getFileContent((0,
|
|
18855
|
+
content = await getFileContent((0, import_node_path18.join)(threatDir, filename));
|
|
18376
18856
|
} catch (e) {
|
|
18377
18857
|
logger.warn(`Failed to read ${filename}`, { error: String(e) });
|
|
18378
18858
|
continue;
|
|
@@ -18398,8 +18878,15 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18398
18878
|
}
|
|
18399
18879
|
ruleFiles.push({ filename, data });
|
|
18400
18880
|
}
|
|
18401
|
-
for (const { filename, data } of ruleFiles) {
|
|
18881
|
+
ruleFilesLoop: for (const { filename, data } of ruleFiles) {
|
|
18402
18882
|
for (const entry of data) {
|
|
18883
|
+
if (threats.length >= maxRules) {
|
|
18884
|
+
logger.warn("Threat rule limit reached; ignoring remaining rules", {
|
|
18885
|
+
path: threatDir,
|
|
18886
|
+
maxRules: MAX_THREAT_RULES
|
|
18887
|
+
});
|
|
18888
|
+
break ruleFilesLoop;
|
|
18889
|
+
}
|
|
18403
18890
|
if (typeof entry !== "object" || entry === null) {
|
|
18404
18891
|
logger.warn(`Skipping non-object entry in ${filename}`);
|
|
18405
18892
|
continue;
|
|
@@ -18411,6 +18898,14 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18411
18898
|
logger.warn(`Skipping threat in ${filename}: missing fields ${missing.join(", ")}`);
|
|
18412
18899
|
continue;
|
|
18413
18900
|
}
|
|
18901
|
+
if (typeof record.id !== "string" || record.id.length === 0) {
|
|
18902
|
+
logger.warn(`Skipping threat in ${filename}: invalid id`, { id: record.id });
|
|
18903
|
+
continue;
|
|
18904
|
+
}
|
|
18905
|
+
if (custom2 && customThreatIds.has(record.id)) {
|
|
18906
|
+
logger.warn(bundledThreatIds.has(record.id) ? `Skipping custom threat ${record.id}: id conflicts with a bundled rule` : `Skipping custom threat ${record.id}: duplicate custom rule id`, { id: record.id, path: (0, import_node_path18.join)(threatDir, filename) });
|
|
18907
|
+
continue;
|
|
18908
|
+
}
|
|
18414
18909
|
if (record.revoked === true)
|
|
18415
18910
|
continue;
|
|
18416
18911
|
if (isExpired(record))
|
|
@@ -18452,7 +18947,7 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18452
18947
|
});
|
|
18453
18948
|
continue;
|
|
18454
18949
|
}
|
|
18455
|
-
|
|
18950
|
+
const threat = {
|
|
18456
18951
|
id: record.id,
|
|
18457
18952
|
version,
|
|
18458
18953
|
detectionName,
|
|
@@ -18465,13 +18960,41 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18465
18960
|
title: record.title,
|
|
18466
18961
|
expiresAt: parseExpiresAt(record.expires_at),
|
|
18467
18962
|
revoked: false,
|
|
18468
|
-
flags
|
|
18469
|
-
|
|
18963
|
+
flags,
|
|
18964
|
+
customRule: custom2
|
|
18965
|
+
};
|
|
18966
|
+
threats.push(threat);
|
|
18967
|
+
if (custom2) {
|
|
18968
|
+
customThreatIds.add(threat.id);
|
|
18969
|
+
if (logCustomRuleIds) {
|
|
18970
|
+
logger.info("Loaded custom threat rule", {
|
|
18971
|
+
id: threat.id,
|
|
18972
|
+
path: (0, import_node_path18.join)(threatDir, filename)
|
|
18973
|
+
});
|
|
18974
|
+
}
|
|
18975
|
+
}
|
|
18470
18976
|
}
|
|
18471
18977
|
}
|
|
18472
18978
|
logger.debug(`Loaded ${threats.length} threats from ${threatDir}`);
|
|
18473
18979
|
return threats;
|
|
18474
18980
|
}
|
|
18981
|
+
async function loadThreats(threatDir, logger = nullLogger, options = {}) {
|
|
18982
|
+
const bundledThreats = await loadThreatDirectory(threatDir, logger);
|
|
18983
|
+
if (!options.customThreatsDir || (0, import_node_path18.resolve)(options.customThreatsDir) === (0, import_node_path18.resolve)(threatDir)) {
|
|
18984
|
+
return bundledThreats;
|
|
18985
|
+
}
|
|
18986
|
+
const bundledThreatIds = new Set(bundledThreats.map((threat) => threat.id));
|
|
18987
|
+
const customThreatIds = new Set(bundledThreatIds);
|
|
18988
|
+
const customThreats = await loadThreatDirectory(options.customThreatsDir, logger, {
|
|
18989
|
+
custom: true,
|
|
18990
|
+
customThreatIds,
|
|
18991
|
+
bundledThreatIds,
|
|
18992
|
+
maxRules: Math.max(0, MAX_THREAT_RULES - bundledThreats.length),
|
|
18993
|
+
logCustomRuleIds: options.logCustomRuleIds,
|
|
18994
|
+
optional: true
|
|
18995
|
+
});
|
|
18996
|
+
return [...bundledThreats, ...customThreats];
|
|
18997
|
+
}
|
|
18475
18998
|
|
|
18476
18999
|
// ../core/dist/evaluator.js
|
|
18477
19000
|
init_types2();
|
|
@@ -18479,6 +19002,8 @@ var AMSI_CONTENT_SNIPPET_MAX = 200;
|
|
|
18479
19002
|
var AMSI_CONTENT_NAME_MAX = 256;
|
|
18480
19003
|
var HEURISTIC_PI_CONTENT_SNIPPET_MAX = 200;
|
|
18481
19004
|
function buildHeuristicSignal(match) {
|
|
19005
|
+
if (match.threat.customRule)
|
|
19006
|
+
return void 0;
|
|
18482
19007
|
return {
|
|
18483
19008
|
detection_name: formatReportingDetectionName(match.threat.detectionName, "heuristics", `${match.threat.id}:${match.threat.version}`),
|
|
18484
19009
|
rule_id: match.threat.id,
|
|
@@ -18494,7 +19019,7 @@ function resolvePiContentSnippet(piResults, heuristicMatches) {
|
|
|
18494
19019
|
}
|
|
18495
19020
|
if (highestRiskResult?.contentSnippet)
|
|
18496
19021
|
return scrubHomePath(highestRiskResult.contentSnippet);
|
|
18497
|
-
const heuristicMatch = heuristicMatches.find((match) => match.threat.category === "prompt_injection");
|
|
19022
|
+
const heuristicMatch = heuristicMatches.find((match) => !match.threat.customRule && match.threat.category === "prompt_injection");
|
|
18498
19023
|
if (!heuristicMatch)
|
|
18499
19024
|
return void 0;
|
|
18500
19025
|
const snippet = safeTruncate(scrubHomePath(heuristicMatch.matchValue), HEURISTIC_PI_CONTENT_SNIPPET_MAX);
|
|
@@ -18545,7 +19070,7 @@ function shouldSkipPromptInjectionForLocalMarkdown(request) {
|
|
|
18545
19070
|
async function evaluateToolCall(request, context) {
|
|
18546
19071
|
const logger = context.logger ?? nullLogger;
|
|
18547
19072
|
const config2 = await resolveEvaluationConfig(context, logger);
|
|
18548
|
-
const eventId = (0,
|
|
19073
|
+
const eventId = (0, import_node_crypto8.randomUUID)();
|
|
18549
19074
|
logger.debug("Tool call evaluation started", {
|
|
18550
19075
|
eventId,
|
|
18551
19076
|
toolUseId: request.toolUseId,
|
|
@@ -18642,7 +19167,9 @@ async function evaluateToolCall(request, context) {
|
|
|
18642
19167
|
const { cachedUrlVerdicts, uncachedUrls } = partitionUrlsByCache(urls, cache2);
|
|
18643
19168
|
let heuristicMatches = [];
|
|
18644
19169
|
if (config2.heuristics_enabled) {
|
|
18645
|
-
let threats = await loadThreats(context.threatsDir, logger
|
|
19170
|
+
let threats = await loadThreats(context.threatsDir, logger, {
|
|
19171
|
+
customThreatsDir: getUserThreatsDir(context.sageDirPath)
|
|
19172
|
+
});
|
|
18646
19173
|
if (config2.disabled_threats.length > 0) {
|
|
18647
19174
|
const disabledSet = new Set(config2.disabled_threats);
|
|
18648
19175
|
threats = threats.filter((t) => !disabledSet.has(t.id));
|
|
@@ -18653,6 +19180,8 @@ async function evaluateToolCall(request, context) {
|
|
|
18653
19180
|
const trustedDomains = await loadTrustedDomains(context.trustedDomainsDir, logger);
|
|
18654
19181
|
const heuristics = new HeuristicsEngine(threats, trustedDomains);
|
|
18655
19182
|
heuristicMatches = heuristics.match(request.artifacts);
|
|
19183
|
+
const rawMatchedIds = new Set(heuristicMatches.map((m) => m.threat.id));
|
|
19184
|
+
heuristicMatches.push(...heuristics.matchDeobfuscated(request.artifacts, rawMatchedIds));
|
|
18656
19185
|
}
|
|
18657
19186
|
let urlCheckResults = [];
|
|
18658
19187
|
const urlsToCheck = cache2 ? uncachedUrls : urls;
|
|
@@ -18729,12 +19258,13 @@ async function evaluateToolCall(request, context) {
|
|
|
18729
19258
|
}
|
|
18730
19259
|
}
|
|
18731
19260
|
const engine = new DecisionEngine(config2.sensitivity, logger);
|
|
18732
|
-
const
|
|
19261
|
+
const preliminarySources = {
|
|
18733
19262
|
heuristicMatches,
|
|
18734
19263
|
urlCheckResults,
|
|
18735
19264
|
packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
|
|
18736
19265
|
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0
|
|
18737
|
-
}
|
|
19266
|
+
};
|
|
19267
|
+
const preliminaryVerdict = await engine.decide(preliminarySources);
|
|
18738
19268
|
const allPiResults = [];
|
|
18739
19269
|
const alreadyDenied = preliminaryVerdict.decision === "deny" || cachedUrlVerdicts.size > 0 && [...cachedUrlVerdicts.values()].some((v) => v.verdict === "deny");
|
|
18740
19270
|
if (config2.pi_check.enabled && !alreadyDenied && request.toolName === "WebFetch") {
|
|
@@ -18768,18 +19298,17 @@ async function evaluateToolCall(request, context) {
|
|
|
18768
19298
|
}
|
|
18769
19299
|
}
|
|
18770
19300
|
let verdict;
|
|
19301
|
+
let verdictSources = preliminarySources;
|
|
18771
19302
|
if (allPiResults.length > 0) {
|
|
18772
|
-
|
|
18773
|
-
|
|
18774
|
-
urlCheckResults,
|
|
18775
|
-
packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
|
|
18776
|
-
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
|
|
19303
|
+
verdictSources = {
|
|
19304
|
+
...preliminarySources,
|
|
18777
19305
|
piCheckResults: allPiResults,
|
|
18778
19306
|
piThresholds: {
|
|
18779
19307
|
highRisk: DEFAULT_PI_HIGH_RISK_THRESHOLD,
|
|
18780
19308
|
mediumRisk: DEFAULT_PI_MEDIUM_RISK_THRESHOLD
|
|
18781
19309
|
}
|
|
18782
|
-
}
|
|
19310
|
+
};
|
|
19311
|
+
verdict = await engine.decide(verdictSources);
|
|
18783
19312
|
} else {
|
|
18784
19313
|
verdict = preliminaryVerdict;
|
|
18785
19314
|
}
|
|
@@ -18804,10 +19333,19 @@ async function evaluateToolCall(request, context) {
|
|
|
18804
19333
|
}
|
|
18805
19334
|
}
|
|
18806
19335
|
}
|
|
19336
|
+
const customThreatMatched = heuristicMatches.some((match) => match.threat.customRule === true);
|
|
19337
|
+
const hasCachedUrlDetection = [...cachedUrlVerdicts.values()].some((cachedVerdict) => cachedVerdict.verdict !== "allow");
|
|
19338
|
+
const nonCustomThreatMatched = customThreatMatched && (engine.hasNonCustomDetection(verdictSources) || hasCachedUrlDetection);
|
|
19339
|
+
verdict.customThreatMatched = customThreatMatched;
|
|
18807
19340
|
await cacheUrlResults(urlCheckResults, cache2);
|
|
18808
19341
|
const auditSignals = {};
|
|
18809
19342
|
if (heuristicMatches.length > 0) {
|
|
18810
|
-
|
|
19343
|
+
const heuristicSignals = heuristicMatches.flatMap((match) => {
|
|
19344
|
+
const signal = buildHeuristicSignal(match);
|
|
19345
|
+
return signal ? [signal] : [];
|
|
19346
|
+
});
|
|
19347
|
+
if (heuristicSignals.length > 0)
|
|
19348
|
+
auditSignals.heuristics = heuristicSignals;
|
|
18811
19349
|
}
|
|
18812
19350
|
if (urlCheckResults.length > 0) {
|
|
18813
19351
|
const relevant = urlCheckResults.filter((r) => r.isMalicious);
|
|
@@ -18855,7 +19393,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18855
19393
|
if (allPiResults.length > 0) {
|
|
18856
19394
|
const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
|
|
18857
19395
|
auditSignals.pi_checks = allPiResults.map((r) => ({
|
|
18858
|
-
detection_name:
|
|
19396
|
+
detection_name: piReportingDetectionName(r),
|
|
18859
19397
|
risk: r.risk,
|
|
18860
19398
|
model_id: r.modelId,
|
|
18861
19399
|
content_name: r.contentName,
|
|
@@ -18878,6 +19416,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18878
19416
|
toolName: request.toolName,
|
|
18879
19417
|
toolInput: request.toolInput,
|
|
18880
19418
|
verdict,
|
|
19419
|
+
nonCustomThreatMatched,
|
|
18881
19420
|
conversationId: request.conversationId,
|
|
18882
19421
|
agentRuntime: request.agentRuntime,
|
|
18883
19422
|
hookType: request.hookType,
|
|
@@ -18890,7 +19429,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18890
19429
|
} catch (error) {
|
|
18891
19430
|
logger.debug("Audit verdict logging failed open", { error: String(error) });
|
|
18892
19431
|
}
|
|
18893
|
-
const submitTelemetry = async (blocking) => {
|
|
19432
|
+
const submitTelemetry = async (blocking, signals = resolvedSignals) => {
|
|
18894
19433
|
try {
|
|
18895
19434
|
await sendCommunityIqTelemetry({
|
|
18896
19435
|
eventId,
|
|
@@ -18900,7 +19439,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18900
19439
|
toolName: request.toolName,
|
|
18901
19440
|
content: resolvedContent,
|
|
18902
19441
|
contentSnippet,
|
|
18903
|
-
signals
|
|
19442
|
+
signals,
|
|
18904
19443
|
communityIqEnabled: config2.community_iq,
|
|
18905
19444
|
config: config2,
|
|
18906
19445
|
blocking,
|
|
@@ -18911,9 +19450,18 @@ async function evaluateToolCall(request, context) {
|
|
|
18911
19450
|
}
|
|
18912
19451
|
};
|
|
18913
19452
|
if (verdict.decision === "deny") {
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
19453
|
+
if (customThreatMatched && !nonCustomThreatMatched) {
|
|
19454
|
+
logger.debug("Custom threat matched without non-custom detections; skipping Community IQ telemetry", { eventId });
|
|
19455
|
+
} else {
|
|
19456
|
+
await submitTelemetry(true);
|
|
19457
|
+
}
|
|
19458
|
+
} else {
|
|
19459
|
+
const piSuspicious = allPiResults.some((r) => r.risk >= DEFAULT_PI_TELEMETRY_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD);
|
|
19460
|
+
const submitOnlyIds = new Set(heuristicMatches.filter((m) => !m.threat.customRule && isSubmitOnlyThreat(m.threat)).map((m) => m.threat.id));
|
|
19461
|
+
const claimedRuleIds = await claimSubmitOnlySubmissions(submitOnlyIds, config2, logger);
|
|
19462
|
+
if (piSuspicious || claimedRuleIds.size > 0) {
|
|
19463
|
+
await submitTelemetry(false, withoutCappedSubmitOnly(resolvedSignals, submitOnlyIds, claimedRuleIds));
|
|
19464
|
+
}
|
|
18917
19465
|
}
|
|
18918
19466
|
if (verdict.decision !== "allow") {
|
|
18919
19467
|
try {
|
|
@@ -18928,6 +19476,26 @@ async function evaluateToolCall(request, context) {
|
|
|
18928
19476
|
logEvaluationCompleted(verdict);
|
|
18929
19477
|
return verdict;
|
|
18930
19478
|
}
|
|
19479
|
+
async function claimSubmitOnlySubmissions(submitOnlyIds, config2, logger) {
|
|
19480
|
+
if (!config2.community_iq || submitOnlyIds.size === 0)
|
|
19481
|
+
return /* @__PURE__ */ new Set();
|
|
19482
|
+
try {
|
|
19483
|
+
return new Set(await claimRuleSubmissions([...submitOnlyIds], { logger }));
|
|
19484
|
+
} catch (error) {
|
|
19485
|
+
logger.debug("Submit-limit claim failed open", { error: String(error) });
|
|
19486
|
+
return /* @__PURE__ */ new Set();
|
|
19487
|
+
}
|
|
19488
|
+
}
|
|
19489
|
+
function withoutCappedSubmitOnly(signals, submitOnlyIds, claimedRuleIds) {
|
|
19490
|
+
if (!signals?.heuristics)
|
|
19491
|
+
return signals;
|
|
19492
|
+
const heuristics = signals.heuristics.filter((h) => !submitOnlyIds.has(h.rule_id) || claimedRuleIds.has(h.rule_id));
|
|
19493
|
+
if (heuristics.length === signals.heuristics.length)
|
|
19494
|
+
return signals;
|
|
19495
|
+
const { heuristics: _dropped, ...rest } = signals;
|
|
19496
|
+
const next = heuristics.length > 0 ? { ...rest, heuristics } : rest;
|
|
19497
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
19498
|
+
}
|
|
18931
19499
|
function partitionUrlsByCache(urls, cache2) {
|
|
18932
19500
|
const cachedUrlVerdicts = /* @__PURE__ */ new Map();
|
|
18933
19501
|
let uncachedUrls = [];
|
|
@@ -19095,14 +19663,19 @@ function ruleLabel(verdict) {
|
|
|
19095
19663
|
return null;
|
|
19096
19664
|
return verdict.source === "exception" ? `${verdict.matchedThreatId} (your exception rule)` : verdict.matchedThreatId;
|
|
19097
19665
|
}
|
|
19666
|
+
var FP_REPORT_TOOL_NAME = "sage_report_false_positive";
|
|
19098
19667
|
function remediationHint(verdict) {
|
|
19099
19668
|
if (verdict.source === "exception") {
|
|
19100
19669
|
const id = verdict.matchedThreatId ? ` (id ${verdict.matchedThreatId})` : "";
|
|
19101
19670
|
return `This block comes from your own rule in ~/.sage/exceptions.json${id} \u2014 edit or remove it there.`;
|
|
19102
19671
|
}
|
|
19672
|
+
if (verdict.customThreat) {
|
|
19673
|
+
return "This match comes from your custom threat rules in ~/.sage/threats/ \u2014 edit or remove the matching rule there.";
|
|
19674
|
+
}
|
|
19103
19675
|
const rule = verdict.matchedThreatId ? ` (rule ${verdict.matchedThreatId})` : "";
|
|
19104
|
-
return `If this is a false positive,
|
|
19676
|
+
return `If this is a false positive, ask your agent to report it with the ${FP_REPORT_TOOL_NAME} MCP tool${rule}.`;
|
|
19105
19677
|
}
|
|
19678
|
+
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.`;
|
|
19106
19679
|
function formatUpdateNotice(result) {
|
|
19107
19680
|
return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/gendigitalinc/sage)`;
|
|
19108
19681
|
}
|
|
@@ -19168,6 +19741,10 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
|
|
|
19168
19741
|
lines.push(` ... and ${overflow} more findings`);
|
|
19169
19742
|
}
|
|
19170
19743
|
}
|
|
19744
|
+
if (!first) {
|
|
19745
|
+
lines.push("");
|
|
19746
|
+
lines.push(SESSION_START_FP_HINT);
|
|
19747
|
+
}
|
|
19171
19748
|
if (versionCheck?.updateAvailable) {
|
|
19172
19749
|
lines.push("");
|
|
19173
19750
|
lines.push(formatUpdateNotice(versionCheck));
|
|
@@ -19190,6 +19767,7 @@ function formatThreatBannerCompact(version, results, versionCheck, branding2) {
|
|
|
19190
19767
|
if (remaining > 0) {
|
|
19191
19768
|
lines.push(`\u2026and ${remaining} more finding${remaining === 1 ? "" : "s"}`);
|
|
19192
19769
|
}
|
|
19770
|
+
lines.push(SESSION_START_FP_HINT);
|
|
19193
19771
|
if (versionCheck?.updateAvailable) {
|
|
19194
19772
|
lines.push(formatUpdateNotice(versionCheck));
|
|
19195
19773
|
}
|
|
@@ -19211,8 +19789,8 @@ function formatSessionStartMessage(version, result, branding2 = defaultBranding,
|
|
|
19211
19789
|
// ../core/dist/guard.js
|
|
19212
19790
|
init_config();
|
|
19213
19791
|
init_types2();
|
|
19214
|
-
async function guardToolCall(request, context, approvalStore) {
|
|
19215
|
-
const actionId = ApprovalStore.actionId(request.toolName, request.toolInput, request.sessionId);
|
|
19792
|
+
async function guardToolCall(request, context, approvalStore, actionIdOverride) {
|
|
19793
|
+
const actionId = actionIdOverride ?? ApprovalStore.actionId(request.toolName, request.toolInput, request.sessionId);
|
|
19216
19794
|
if (approvalStore.isApproved(actionId)) {
|
|
19217
19795
|
return { verdict: allowVerdict("approved") };
|
|
19218
19796
|
}
|
|
@@ -19261,7 +19839,7 @@ function formatDenyMessage(verdict, branding2 = defaultBranding) {
|
|
|
19261
19839
|
}
|
|
19262
19840
|
|
|
19263
19841
|
// ../core/dist/install-state.js
|
|
19264
|
-
var
|
|
19842
|
+
var import_node_path19 = require("node:path");
|
|
19265
19843
|
init_config();
|
|
19266
19844
|
init_file_utils();
|
|
19267
19845
|
|
|
@@ -19299,7 +19877,7 @@ init_types2();
|
|
|
19299
19877
|
var INSTALL_STATE_SCHEMA_VERSION = 1;
|
|
19300
19878
|
var SKILL_UPLOAD_NOTICE_ID = SKILL_UPLOAD_NOTICE.id;
|
|
19301
19879
|
function defaultStatePath(sageDirPath) {
|
|
19302
|
-
return (0,
|
|
19880
|
+
return (0, import_node_path19.join)(sageDirPath ?? resolvePath(SAGE_DIR), "install-state.json");
|
|
19303
19881
|
}
|
|
19304
19882
|
function emptyState() {
|
|
19305
19883
|
return { schemaVersion: INSTALL_STATE_SCHEMA_VERSION, notices: {} };
|
|
@@ -19539,8 +20117,8 @@ function createOperationalLogger(config2, runtime) {
|
|
|
19539
20117
|
try {
|
|
19540
20118
|
const result = await Promise.race([
|
|
19541
20119
|
Promise.allSettled([...pendingWrites]).then(() => "drained"),
|
|
19542
|
-
new Promise((
|
|
19543
|
-
timeout = setTimeout(() =>
|
|
20120
|
+
new Promise((resolve11) => {
|
|
20121
|
+
timeout = setTimeout(() => resolve11("timeout"), remainingMs);
|
|
19544
20122
|
timeout.unref?.();
|
|
19545
20123
|
})
|
|
19546
20124
|
]);
|
|
@@ -19588,32 +20166,32 @@ function createOperationalLogger(config2, runtime) {
|
|
|
19588
20166
|
}
|
|
19589
20167
|
|
|
19590
20168
|
// ../core/dist/plugin-scan-cache.js
|
|
19591
|
-
var
|
|
19592
|
-
var
|
|
19593
|
-
var
|
|
20169
|
+
var import_node_crypto9 = require("node:crypto");
|
|
20170
|
+
var import_promises10 = require("node:fs/promises");
|
|
20171
|
+
var import_node_path20 = require("node:path");
|
|
19594
20172
|
init_config();
|
|
19595
20173
|
init_file_utils();
|
|
19596
20174
|
init_types2();
|
|
19597
|
-
var DEFAULT_CACHE_PATH = (0,
|
|
20175
|
+
var DEFAULT_CACHE_PATH = (0, import_node_path20.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
|
|
19598
20176
|
var DEFAULT_CACHE_TTL_MS = MS_PER_DAY;
|
|
19599
20177
|
var SCHEMA_VERSION = 5;
|
|
19600
20178
|
function cacheKey(pluginKey, version, lastUpdated) {
|
|
19601
20179
|
return `${pluginKey}:${version}:${lastUpdated}`;
|
|
19602
20180
|
}
|
|
19603
20181
|
async function computeConfigHash(sageVersion, ...dirs) {
|
|
19604
|
-
const h = (0,
|
|
20182
|
+
const h = (0, import_node_crypto9.createHash)("sha256");
|
|
19605
20183
|
if (sageVersion)
|
|
19606
20184
|
h.update(sageVersion);
|
|
19607
20185
|
for (const dir of dirs) {
|
|
19608
20186
|
let files;
|
|
19609
20187
|
try {
|
|
19610
|
-
files = (await (0,
|
|
20188
|
+
files = (await (0, import_promises10.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
19611
20189
|
} catch {
|
|
19612
20190
|
continue;
|
|
19613
20191
|
}
|
|
19614
20192
|
for (const file of files) {
|
|
19615
20193
|
try {
|
|
19616
|
-
const content = await getFileContent((0,
|
|
20194
|
+
const content = await getFileContent((0, import_node_path20.join)(dir, file));
|
|
19617
20195
|
h.update(content);
|
|
19618
20196
|
} catch {
|
|
19619
20197
|
}
|
|
@@ -19712,22 +20290,22 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
|
|
|
19712
20290
|
}
|
|
19713
20291
|
|
|
19714
20292
|
// ../core/dist/plugin-scanner.js
|
|
19715
|
-
var
|
|
19716
|
-
var
|
|
19717
|
-
var
|
|
20293
|
+
var import_node_crypto12 = require("node:crypto");
|
|
20294
|
+
var import_promises13 = require("node:fs/promises");
|
|
20295
|
+
var import_node_path24 = require("node:path");
|
|
19718
20296
|
init_config();
|
|
19719
20297
|
init_file_utils();
|
|
19720
20298
|
|
|
19721
20299
|
// ../core/dist/skill-id.js
|
|
19722
|
-
var
|
|
19723
|
-
var
|
|
19724
|
-
var
|
|
20300
|
+
var import_node_crypto10 = require("node:crypto");
|
|
20301
|
+
var import_promises11 = require("node:fs/promises");
|
|
20302
|
+
var import_node_path21 = require("node:path");
|
|
19725
20303
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
|
|
19726
20304
|
var MAX_SKILL_BYTES = 50 * 1024 * 1024;
|
|
19727
20305
|
var SkillTooLargeError = class extends Error {
|
|
19728
20306
|
};
|
|
19729
20307
|
function isContained(childReal, rootReal) {
|
|
19730
|
-
return childReal === rootReal || childReal.startsWith(`${rootReal}${
|
|
20308
|
+
return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path21.sep}`);
|
|
19731
20309
|
}
|
|
19732
20310
|
function normalizePath(p) {
|
|
19733
20311
|
let out = p.replace(/\\/g, "/").normalize("NFC");
|
|
@@ -19738,24 +20316,24 @@ function normalizePath(p) {
|
|
|
19738
20316
|
return out;
|
|
19739
20317
|
}
|
|
19740
20318
|
async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY) {
|
|
19741
|
-
const absDir = (0,
|
|
19742
|
-
const rootReal = await (0,
|
|
20319
|
+
const absDir = (0, import_node_path21.resolve)(dirPath);
|
|
20320
|
+
const rootReal = await (0, import_promises11.realpath)(absDir);
|
|
19743
20321
|
const entries = [];
|
|
19744
20322
|
const visited = /* @__PURE__ */ new Set();
|
|
19745
20323
|
let total = 0;
|
|
19746
20324
|
async function walk(currentPath) {
|
|
19747
|
-
const real = await (0,
|
|
20325
|
+
const real = await (0, import_promises11.realpath)(currentPath);
|
|
19748
20326
|
if (visited.has(real))
|
|
19749
20327
|
return;
|
|
19750
20328
|
if (!isContained(real, rootReal))
|
|
19751
20329
|
return;
|
|
19752
20330
|
visited.add(real);
|
|
19753
|
-
const items = await (0,
|
|
20331
|
+
const items = await (0, import_promises11.readdir)(currentPath);
|
|
19754
20332
|
const dirs = [];
|
|
19755
20333
|
const files = [];
|
|
19756
20334
|
for (const item of items) {
|
|
19757
|
-
const fullPath = (0,
|
|
19758
|
-
const st = await (0,
|
|
20335
|
+
const fullPath = (0, import_node_path21.join)(currentPath, item);
|
|
20336
|
+
const st = await (0, import_promises11.stat)(fullPath);
|
|
19759
20337
|
if (st.isDirectory())
|
|
19760
20338
|
dirs.push(item);
|
|
19761
20339
|
else if (st.isFile())
|
|
@@ -19764,24 +20342,24 @@ async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY
|
|
|
19764
20342
|
dirs.sort();
|
|
19765
20343
|
files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
19766
20344
|
for (const d of dirs) {
|
|
19767
|
-
const fullPath = (0,
|
|
19768
|
-
const relPath = (0,
|
|
20345
|
+
const fullPath = (0, import_node_path21.join)(currentPath, d);
|
|
20346
|
+
const relPath = (0, import_node_path21.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19769
20347
|
entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
|
|
19770
20348
|
await walk(fullPath);
|
|
19771
20349
|
}
|
|
19772
20350
|
for (const f of files) {
|
|
19773
|
-
const fullPath = (0,
|
|
19774
|
-
const fileReal = await (0,
|
|
20351
|
+
const fullPath = (0, import_node_path21.join)(currentPath, f.name);
|
|
20352
|
+
const fileReal = await (0, import_promises11.realpath)(fullPath);
|
|
19775
20353
|
if (!isContained(fileReal, rootReal))
|
|
19776
20354
|
continue;
|
|
19777
20355
|
total += f.size;
|
|
19778
20356
|
if (total > maxBytes)
|
|
19779
20357
|
throw new SkillTooLargeError();
|
|
19780
|
-
const relPath = (0,
|
|
20358
|
+
const relPath = (0, import_node_path21.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19781
20359
|
entries.push({
|
|
19782
20360
|
entryPath: relPath,
|
|
19783
20361
|
isDir: false,
|
|
19784
|
-
content: await (0,
|
|
20362
|
+
content: await (0, import_promises11.readFile)(fullPath)
|
|
19785
20363
|
});
|
|
19786
20364
|
}
|
|
19787
20365
|
}
|
|
@@ -19804,14 +20382,14 @@ function computeSkillId(entries) {
|
|
|
19804
20382
|
const fileHashes = {};
|
|
19805
20383
|
const treeEntries = [];
|
|
19806
20384
|
for (const e of normalized) {
|
|
19807
|
-
const contentHash = (0,
|
|
20385
|
+
const contentHash = (0, import_node_crypto10.createHash)("sha256").update(e.isDir ? Buffer.alloc(0) : e.content).digest("hex");
|
|
19808
20386
|
const entryType = e.isDir ? "dir" : "file";
|
|
19809
20387
|
treeEntries.push({ type: entryType, entryPath: e.entryPath, hash: contentHash });
|
|
19810
20388
|
if (!e.isDir)
|
|
19811
20389
|
fileHashes[e.entryPath] = contentHash;
|
|
19812
20390
|
}
|
|
19813
20391
|
treeEntries.sort((a, b) => a.entryPath < b.entryPath ? -1 : a.entryPath > b.entryPath ? 1 : 0);
|
|
19814
|
-
const skillHasher = (0,
|
|
20392
|
+
const skillHasher = (0, import_node_crypto10.createHash)("sha256");
|
|
19815
20393
|
for (const e of treeEntries) {
|
|
19816
20394
|
skillHasher.update(Buffer.from(`${e.type}\0${e.entryPath}\0${e.hash}
|
|
19817
20395
|
`, "utf8"));
|
|
@@ -19822,7 +20400,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19822
20400
|
const found = [];
|
|
19823
20401
|
let rootReal;
|
|
19824
20402
|
try {
|
|
19825
|
-
rootReal = await (0,
|
|
20403
|
+
rootReal = await (0, import_promises11.realpath)((0, import_node_path21.resolve)(rootDir));
|
|
19826
20404
|
} catch {
|
|
19827
20405
|
return found;
|
|
19828
20406
|
}
|
|
@@ -19830,7 +20408,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19830
20408
|
async function walk(currentPath) {
|
|
19831
20409
|
let real;
|
|
19832
20410
|
try {
|
|
19833
|
-
real = await (0,
|
|
20411
|
+
real = await (0, import_promises11.realpath)(currentPath);
|
|
19834
20412
|
} catch {
|
|
19835
20413
|
return 0;
|
|
19836
20414
|
}
|
|
@@ -19841,7 +20419,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19841
20419
|
visited.add(real);
|
|
19842
20420
|
let st;
|
|
19843
20421
|
try {
|
|
19844
|
-
st = await (0,
|
|
20422
|
+
st = await (0, import_promises11.stat)(currentPath);
|
|
19845
20423
|
} catch {
|
|
19846
20424
|
return 0;
|
|
19847
20425
|
}
|
|
@@ -19850,7 +20428,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19850
20428
|
return subtreeMax;
|
|
19851
20429
|
let items;
|
|
19852
20430
|
try {
|
|
19853
|
-
items = await (0,
|
|
20431
|
+
items = await (0, import_promises11.readdir)(currentPath);
|
|
19854
20432
|
} catch {
|
|
19855
20433
|
return subtreeMax;
|
|
19856
20434
|
}
|
|
@@ -19858,7 +20436,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19858
20436
|
let entry;
|
|
19859
20437
|
if (items.includes("SKILL.md")) {
|
|
19860
20438
|
try {
|
|
19861
|
-
if ((await (0,
|
|
20439
|
+
if ((await (0, import_promises11.stat)((0, import_node_path21.join)(currentPath, "SKILL.md"))).isFile()) {
|
|
19862
20440
|
entry = { folder: currentPath, newestMtimeMs: 0 };
|
|
19863
20441
|
found.push(entry);
|
|
19864
20442
|
}
|
|
@@ -19868,7 +20446,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19868
20446
|
for (const item of items) {
|
|
19869
20447
|
if (SKIP_DIRS.has(item))
|
|
19870
20448
|
continue;
|
|
19871
|
-
const childMax = await walk((0,
|
|
20449
|
+
const childMax = await walk((0, import_node_path21.join)(currentPath, item));
|
|
19872
20450
|
if (childMax > subtreeMax)
|
|
19873
20451
|
subtreeMax = childMax;
|
|
19874
20452
|
}
|
|
@@ -19876,16 +20454,16 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19876
20454
|
entry.newestMtimeMs = subtreeMax;
|
|
19877
20455
|
return subtreeMax;
|
|
19878
20456
|
}
|
|
19879
|
-
await walk((0,
|
|
20457
|
+
await walk((0, import_node_path21.resolve)(rootDir));
|
|
19880
20458
|
return found;
|
|
19881
20459
|
}
|
|
19882
20460
|
function looseSkillKey(skillsRoot, folder, tag, scope) {
|
|
19883
|
-
const rel = (0,
|
|
20461
|
+
const rel = (0, import_node_path21.relative)((0, import_node_path21.resolve)(skillsRoot), (0, import_node_path21.resolve)(folder)).split(import_node_path21.sep).join("/") || (0, import_node_path21.basename)(folder);
|
|
19884
20462
|
return `skill:${tag}/${rel}@${scope}`;
|
|
19885
20463
|
}
|
|
19886
20464
|
async function discoverLooseSkills(skillsDir, tag, scope) {
|
|
19887
|
-
const resolvedRoot = (0,
|
|
19888
|
-
const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0,
|
|
20465
|
+
const resolvedRoot = (0, import_node_path21.resolve)(skillsDir);
|
|
20466
|
+
const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0, import_node_path21.resolve)(p.folder) !== resolvedRoot);
|
|
19889
20467
|
return packages.map(({ folder, newestMtimeMs }) => ({
|
|
19890
20468
|
key: looseSkillKey(resolvedRoot, folder, tag, scope),
|
|
19891
20469
|
installPath: folder,
|
|
@@ -19899,7 +20477,7 @@ async function dedupeSkillsByResolvedPath(skills) {
|
|
|
19899
20477
|
for (const skill of skills) {
|
|
19900
20478
|
let canonical = skill.installPath;
|
|
19901
20479
|
try {
|
|
19902
|
-
canonical = await (0,
|
|
20480
|
+
canonical = await (0, import_promises11.realpath)(skill.installPath);
|
|
19903
20481
|
} catch {
|
|
19904
20482
|
}
|
|
19905
20483
|
if (seen.has(canonical))
|
|
@@ -19941,15 +20519,15 @@ async function computeSkillIdsForRoot(rootDir, logger) {
|
|
|
19941
20519
|
}
|
|
19942
20520
|
|
|
19943
20521
|
// ../core/dist/skill-pending.js
|
|
19944
|
-
var
|
|
19945
|
-
var
|
|
20522
|
+
var import_promises12 = require("node:fs/promises");
|
|
20523
|
+
var import_node_path22 = require("node:path");
|
|
19946
20524
|
init_config();
|
|
19947
20525
|
init_file_utils();
|
|
19948
20526
|
init_types2();
|
|
19949
20527
|
var SCHEMA_VERSION2 = 1;
|
|
19950
20528
|
var PENDING_TTL_MS = 60 * 60 * 1e3;
|
|
19951
20529
|
function defaultPendingPath() {
|
|
19952
|
-
return (0,
|
|
20530
|
+
return (0, import_node_path22.join)(resolvePath(SAGE_DIR), "skill_pending.json");
|
|
19953
20531
|
}
|
|
19954
20532
|
async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19955
20533
|
let raw;
|
|
@@ -19987,7 +20565,7 @@ async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nu
|
|
|
19987
20565
|
async function savePendingMarker(marker, pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19988
20566
|
if (Object.keys(marker.entries).length === 0) {
|
|
19989
20567
|
try {
|
|
19990
|
-
await (0,
|
|
20568
|
+
await (0, import_promises12.unlink)(pendingPath);
|
|
19991
20569
|
} catch (e) {
|
|
19992
20570
|
if (!e || typeof e !== "object" || e.code !== "ENOENT") {
|
|
19993
20571
|
logger.warn(`Failed to delete empty pending marker at ${pendingPath}`, {
|
|
@@ -20039,7 +20617,7 @@ function listPending(marker) {
|
|
|
20039
20617
|
}
|
|
20040
20618
|
|
|
20041
20619
|
// ../core/dist/skill-verdict-cache.js
|
|
20042
|
-
var
|
|
20620
|
+
var import_node_path23 = require("node:path");
|
|
20043
20621
|
init_config();
|
|
20044
20622
|
init_file_utils();
|
|
20045
20623
|
init_types2();
|
|
@@ -20075,7 +20653,7 @@ function mergeSources(existing, incoming) {
|
|
|
20075
20653
|
return merged.length > 0 ? merged : void 0;
|
|
20076
20654
|
}
|
|
20077
20655
|
function defaultCachePath2() {
|
|
20078
|
-
return (0,
|
|
20656
|
+
return (0, import_node_path23.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
|
|
20079
20657
|
}
|
|
20080
20658
|
async function loadSkillVerdictCacheInternal(cachePath, ttlMs, logger, pruneOnLoad) {
|
|
20081
20659
|
let raw;
|
|
@@ -20169,9 +20747,9 @@ function putVerdict(cache2, skillId, verdict) {
|
|
|
20169
20747
|
}
|
|
20170
20748
|
|
|
20171
20749
|
// ../core/dist/skill-verdict-record.js
|
|
20172
|
-
var
|
|
20750
|
+
var import_node_crypto11 = require("node:crypto");
|
|
20173
20751
|
function recordAnalyzedSkillVerdict(cache2, skillId, result, origin) {
|
|
20174
|
-
const entryId = (0,
|
|
20752
|
+
const entryId = (0, import_node_crypto11.randomUUID)();
|
|
20175
20753
|
putVerdict(cache2, skillId, {
|
|
20176
20754
|
entryId,
|
|
20177
20755
|
jobId: result.jobId,
|
|
@@ -20223,7 +20801,7 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20223
20801
|
const visited = /* @__PURE__ */ new Set();
|
|
20224
20802
|
let rootReal;
|
|
20225
20803
|
try {
|
|
20226
|
-
rootReal = await (0,
|
|
20804
|
+
rootReal = await (0, import_promises13.realpath)((0, import_node_path24.resolve)(installPath));
|
|
20227
20805
|
} catch (e) {
|
|
20228
20806
|
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
20229
20807
|
return files;
|
|
@@ -20231,7 +20809,7 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20231
20809
|
async function walk(dirOrFile) {
|
|
20232
20810
|
let real;
|
|
20233
20811
|
try {
|
|
20234
|
-
real = await (0,
|
|
20812
|
+
real = await (0, import_promises13.realpath)(dirOrFile);
|
|
20235
20813
|
} catch {
|
|
20236
20814
|
return;
|
|
20237
20815
|
}
|
|
@@ -20242,12 +20820,12 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20242
20820
|
visited.add(real);
|
|
20243
20821
|
let stats;
|
|
20244
20822
|
try {
|
|
20245
|
-
stats = await (0,
|
|
20823
|
+
stats = await (0, import_promises13.stat)(dirOrFile);
|
|
20246
20824
|
} catch {
|
|
20247
20825
|
return;
|
|
20248
20826
|
}
|
|
20249
20827
|
if (stats.isFile()) {
|
|
20250
|
-
if (SCANNABLE_EXTENSIONS2.has((0,
|
|
20828
|
+
if (SCANNABLE_EXTENSIONS2.has((0, import_node_path24.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
|
|
20251
20829
|
files.push(dirOrFile);
|
|
20252
20830
|
}
|
|
20253
20831
|
return;
|
|
@@ -20255,20 +20833,20 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20255
20833
|
if (stats.isDirectory()) {
|
|
20256
20834
|
let entries;
|
|
20257
20835
|
try {
|
|
20258
|
-
entries = await (0,
|
|
20836
|
+
entries = await (0, import_promises13.readdir)(dirOrFile);
|
|
20259
20837
|
} catch {
|
|
20260
20838
|
return;
|
|
20261
20839
|
}
|
|
20262
20840
|
for (const entry of entries) {
|
|
20263
20841
|
if (SKIP_DIRS.has(entry))
|
|
20264
20842
|
continue;
|
|
20265
|
-
const fullPath = (0,
|
|
20843
|
+
const fullPath = (0, import_node_path24.join)(dirOrFile, entry);
|
|
20266
20844
|
await walk(fullPath);
|
|
20267
20845
|
}
|
|
20268
20846
|
}
|
|
20269
20847
|
}
|
|
20270
20848
|
try {
|
|
20271
|
-
await walk((0,
|
|
20849
|
+
await walk((0, import_node_path24.resolve)(installPath));
|
|
20272
20850
|
} catch (e) {
|
|
20273
20851
|
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
20274
20852
|
}
|
|
@@ -20305,7 +20883,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20305
20883
|
}
|
|
20306
20884
|
if (amsiClient) {
|
|
20307
20885
|
try {
|
|
20308
|
-
const scanName = `${plugin.key}/${(0,
|
|
20886
|
+
const scanName = `${plugin.key}/${(0, import_node_path24.relative)(plugin.installPath, filePath)}`;
|
|
20309
20887
|
const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
|
|
20310
20888
|
if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
|
|
20311
20889
|
result.findings.push({
|
|
@@ -20313,7 +20891,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20313
20891
|
title: `AMSI detection (result=${amsiResult.amsiResult})`,
|
|
20314
20892
|
severity: "critical",
|
|
20315
20893
|
artifact: content.slice(0, 200),
|
|
20316
|
-
sourceFile: (0,
|
|
20894
|
+
sourceFile: (0, import_node_path24.relative)(plugin.installPath, filePath)
|
|
20317
20895
|
});
|
|
20318
20896
|
}
|
|
20319
20897
|
} catch {
|
|
@@ -20323,7 +20901,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20323
20901
|
allUrls.push(...extractUrls(content));
|
|
20324
20902
|
}
|
|
20325
20903
|
if (checkFileHashes) {
|
|
20326
|
-
const sha256 = (0,
|
|
20904
|
+
const sha256 = (0, import_node_crypto12.createHash)("sha256").update(rawBytes).digest("hex");
|
|
20327
20905
|
const existing = hashToFiles.get(sha256);
|
|
20328
20906
|
if (existing) {
|
|
20329
20907
|
existing.push(filePath);
|
|
@@ -20366,7 +20944,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20366
20944
|
title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
|
|
20367
20945
|
severity: "critical",
|
|
20368
20946
|
artifact: fr.sha256,
|
|
20369
|
-
sourceFile: (0,
|
|
20947
|
+
sourceFile: (0, import_node_path24.relative)(plugin.installPath, filePath)
|
|
20370
20948
|
});
|
|
20371
20949
|
}
|
|
20372
20950
|
}
|
|
@@ -20393,7 +20971,7 @@ function pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict) {
|
|
|
20393
20971
|
title: verdict.summary?.trim() || (risk === "CRITICAL" ? "Malicious skill detected" : "Suspicious skill detected"),
|
|
20394
20972
|
severity,
|
|
20395
20973
|
artifact: skillId.slice(0, 16),
|
|
20396
|
-
sourceFile: (0,
|
|
20974
|
+
sourceFile: (0, import_node_path24.relative)(plugin.installPath, folder) || "."
|
|
20397
20975
|
});
|
|
20398
20976
|
}
|
|
20399
20977
|
async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
@@ -20431,7 +21009,7 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
|
20431
21009
|
if (verdict) {
|
|
20432
21010
|
if (!recordedVerdicts.has(skillId)) {
|
|
20433
21011
|
recordedVerdicts.set(skillId, recordAnalyzedSkillVerdict(verdictCache, skillId, verdict, {
|
|
20434
|
-
skillName: (0,
|
|
21012
|
+
skillName: (0, import_node_path24.basename)(folder),
|
|
20435
21013
|
agentRuntime: paths.agentRuntime,
|
|
20436
21014
|
containerKey: plugin.key,
|
|
20437
21015
|
loggingConfig: paths.loggingConfig
|
|
@@ -20483,12 +21061,12 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
|
20483
21061
|
// ../core/dist/session-start.js
|
|
20484
21062
|
var import_node_child_process2 = require("node:child_process");
|
|
20485
21063
|
var import_node_fs4 = require("node:fs");
|
|
20486
|
-
var
|
|
21064
|
+
var import_node_path26 = require("node:path");
|
|
20487
21065
|
init_config();
|
|
20488
21066
|
init_file_utils();
|
|
20489
21067
|
|
|
20490
21068
|
// ../core/dist/session-start-scan.js
|
|
20491
|
-
var
|
|
21069
|
+
var import_node_path25 = require("node:path");
|
|
20492
21070
|
init_config();
|
|
20493
21071
|
init_types2();
|
|
20494
21072
|
function fromCachedFinding(finding) {
|
|
@@ -20571,8 +21149,8 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
20571
21149
|
deniedByException: 0,
|
|
20572
21150
|
findingsCount: 0
|
|
20573
21151
|
};
|
|
20574
|
-
const skillPendingPath = context.sageDirPath ? (0,
|
|
20575
|
-
const skillVerdictCachePath = context.sageDirPath ? (0,
|
|
21152
|
+
const skillPendingPath = context.sageDirPath ? (0, import_node_path25.join)(context.sageDirPath, "skill_pending.json") : void 0;
|
|
21153
|
+
const skillVerdictCachePath = context.sageDirPath ? (0, import_node_path25.join)(context.sageDirPath, "skill_verdict_cache.json") : void 0;
|
|
20576
21154
|
const cacheTtlMs = skillCacheTtlMs(config2);
|
|
20577
21155
|
for (const plugin of plugins) {
|
|
20578
21156
|
const denyMatch = findPluginDenyException(exceptions, plugin.key);
|
|
@@ -20738,6 +21316,16 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
|
|
|
20738
21316
|
async function runSessionStart(ctx) {
|
|
20739
21317
|
const logger = ctx.logger ?? nullLogger;
|
|
20740
21318
|
const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
|
|
21319
|
+
try {
|
|
21320
|
+
await loadThreats(ctx.threatsDir, logger, {
|
|
21321
|
+
customThreatsDir: getUserThreatsDir(sageDirPath),
|
|
21322
|
+
logCustomRuleIds: true
|
|
21323
|
+
});
|
|
21324
|
+
} catch (error) {
|
|
21325
|
+
logger.warn("Threat rules failed to load at session start (fail-open)", {
|
|
21326
|
+
error: String(error)
|
|
21327
|
+
});
|
|
21328
|
+
}
|
|
20741
21329
|
pruneOrphanedTmpFiles(sageDirPath).catch(() => {
|
|
20742
21330
|
});
|
|
20743
21331
|
deployConfigDefaults(sageDirPath, logger).catch(() => {
|
|
@@ -20920,7 +21508,7 @@ async function maybeSpawnSkillUploadWorker(args) {
|
|
|
20920
21508
|
});
|
|
20921
21509
|
return;
|
|
20922
21510
|
}
|
|
20923
|
-
const marker = await loadPendingMarker((0,
|
|
21511
|
+
const marker = await loadPendingMarker((0, import_node_path26.join)(args.sageDirPath, "skill_pending.json"), args.logger);
|
|
20924
21512
|
const pendingCount = listPending(marker).length;
|
|
20925
21513
|
if (pendingCount === 0) {
|
|
20926
21514
|
args.logger.debug("Skill upload worker skipped", {
|
|
@@ -21059,19 +21647,19 @@ function canonicalizeToolName(map, rawName) {
|
|
|
21059
21647
|
init_types2();
|
|
21060
21648
|
|
|
21061
21649
|
// src/bundled-dirs.ts
|
|
21062
|
-
var
|
|
21063
|
-
var packageRoot = (0,
|
|
21650
|
+
var import_node_path27 = require("node:path");
|
|
21651
|
+
var packageRoot = (0, import_node_path27.resolve)(__dirname, "..");
|
|
21064
21652
|
function getBundledDataDirs() {
|
|
21065
21653
|
return {
|
|
21066
|
-
threatsDir: (0,
|
|
21067
|
-
trustedDomainsDir: (0,
|
|
21654
|
+
threatsDir: (0, import_node_path27.join)(packageRoot, "resources", "threats"),
|
|
21655
|
+
trustedDomainsDir: (0, import_node_path27.join)(packageRoot, "resources", "trusted-domains")
|
|
21068
21656
|
};
|
|
21069
21657
|
}
|
|
21070
21658
|
var cachedVersion;
|
|
21071
21659
|
function getSageVersion() {
|
|
21072
21660
|
if (cachedVersion) return cachedVersion;
|
|
21073
21661
|
try {
|
|
21074
|
-
const pkg = JSON.parse(getFileContentSync((0,
|
|
21662
|
+
const pkg = JSON.parse(getFileContentSync((0, import_node_path27.join)(packageRoot, "package.json"), "utf-8"));
|
|
21075
21663
|
cachedVersion = pkg.version ?? "0.0.0";
|
|
21076
21664
|
} catch {
|
|
21077
21665
|
cachedVersion = "0.0.0";
|
|
@@ -21080,16 +21668,16 @@ function getSageVersion() {
|
|
|
21080
21668
|
}
|
|
21081
21669
|
|
|
21082
21670
|
// src/startup-scan.ts
|
|
21083
|
-
var
|
|
21671
|
+
var import_node_path29 = require("node:path");
|
|
21084
21672
|
|
|
21085
21673
|
// src/plugin-discovery.ts
|
|
21086
|
-
var
|
|
21674
|
+
var import_promises14 = require("node:fs/promises");
|
|
21087
21675
|
var import_node_os4 = require("node:os");
|
|
21088
|
-
var
|
|
21089
|
-
var DEFAULT_EXTENSIONS_DIR = (0,
|
|
21676
|
+
var import_node_path28 = require("node:path");
|
|
21677
|
+
var DEFAULT_EXTENSIONS_DIR = (0, import_node_path28.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
|
|
21090
21678
|
var SKILL_ROOTS = [
|
|
21091
|
-
{ dir: (0,
|
|
21092
|
-
{ dir: (0,
|
|
21679
|
+
{ dir: (0, import_node_path28.join)(getHomeDir(), ".agents", "skills"), tag: "agents", scope: "personal" },
|
|
21680
|
+
{ dir: (0, import_node_path28.join)(getHomeDir(), ".openclaw", "skills"), tag: "openclaw", scope: "personal" }
|
|
21093
21681
|
];
|
|
21094
21682
|
async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding, skillRoots = SKILL_ROOTS) {
|
|
21095
21683
|
logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
|
|
@@ -21098,23 +21686,23 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
21098
21686
|
const plugins = [];
|
|
21099
21687
|
let entries;
|
|
21100
21688
|
try {
|
|
21101
|
-
entries = await (0,
|
|
21689
|
+
entries = await (0, import_promises14.readdir)(extensionsDir);
|
|
21102
21690
|
} catch {
|
|
21103
21691
|
logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
|
|
21104
21692
|
entries = [];
|
|
21105
21693
|
}
|
|
21106
21694
|
for (const entry of entries) {
|
|
21107
|
-
const extDir = (0,
|
|
21695
|
+
const extDir = (0, import_node_path28.join)(extensionsDir, entry);
|
|
21108
21696
|
let stats;
|
|
21109
21697
|
try {
|
|
21110
|
-
stats = await (0,
|
|
21698
|
+
stats = await (0, import_promises14.stat)(extDir);
|
|
21111
21699
|
if (!stats.isDirectory()) continue;
|
|
21112
21700
|
} catch {
|
|
21113
21701
|
continue;
|
|
21114
21702
|
}
|
|
21115
21703
|
let pkg;
|
|
21116
21704
|
try {
|
|
21117
|
-
const pkgPath = (0,
|
|
21705
|
+
const pkgPath = (0, import_node_path28.join)(extDir, "package.json");
|
|
21118
21706
|
const raw = await getFileContent(pkgPath);
|
|
21119
21707
|
pkg = JSON.parse(raw);
|
|
21120
21708
|
} catch {
|
|
@@ -21161,8 +21749,8 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
|
|
|
21161
21749
|
// No onNotices: OpenClaw delivers the one-time notice from
|
|
21162
21750
|
// `before_prompt_build` (a different process/turn than this scan) via
|
|
21163
21751
|
// takePendingNotices, so the scan-time callback would never fire.
|
|
21164
|
-
modelDownloadWorkerPath: (0,
|
|
21165
|
-
skillUploadWorkerPath: (0,
|
|
21752
|
+
modelDownloadWorkerPath: (0, import_node_path29.resolve)(__dirname, "model-download-worker.cjs"),
|
|
21753
|
+
skillUploadWorkerPath: (0, import_node_path29.resolve)(__dirname, "skill-upload-worker.cjs"),
|
|
21166
21754
|
// Don't commit the "shown" flag here — commit happens at delivery.
|
|
21167
21755
|
commitUploadNotice: false,
|
|
21168
21756
|
announceCleanScans
|