@gendigital/sage-openclaw 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +1648 -489
- package/dist/index.cjs.map +4 -4
- package/dist/mcp-server.cjs +230 -96
- package/dist/mcp-server.cjs.map +4 -4
- package/dist/model-download-worker.cjs +139 -94
- package/dist/model-download-worker.cjs.map +3 -3
- package/dist/skill-upload-worker.cjs +5638 -0
- package/dist/skill-upload-worker.cjs.map +7 -0
- package/openclaw.plugin.json +6 -1
- package/package.json +8 -3
- package/resources/threats/_macros.yaml +193 -0
- package/resources/threats/agent-layer.yaml +19 -1
- package/resources/threats/commands.yaml +165 -31
- package/resources/threats/credentials.yaml +24 -4
- package/resources/threats/dummy.yaml +52 -17
- package/resources/threats/files.yaml +38 -0
- package/resources/threats/mac-commands.yaml +118 -27
- package/resources/threats/mac-credentials.yaml +34 -8
- package/resources/threats/mac-files.yaml +20 -0
- package/resources/threats/mac-obfuscation.yaml +28 -6
- package/resources/threats/mac-persistence.yaml +30 -4
- package/resources/threats/mac-supply-chain.yaml +12 -3
- package/resources/threats/mitre.yaml +325 -62
- package/resources/threats/obfuscation.yaml +33 -8
- package/resources/threats/persistence.yaml +40 -5
- package/resources/threats/prompt-injection.yaml +58 -0
- package/resources/threats/self-defense.yaml +15 -2
- package/resources/threats/supply_chain.yaml +6 -1
- package/resources/threats/urls.yaml +10 -0
- package/resources/threats/win-commands.yaml +294 -60
- package/resources/threats/win-credentials.yaml +68 -9
- package/resources/threats/win-files.yaml +20 -0
- package/resources/threats/win-obfuscation.yaml +56 -8
- package/resources/threats/win-persistence.yaml +45 -7
- package/resources/threats/win-supply-chain.yaml +5 -1
package/dist/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((resolve10) => setTimeout(resolve10, 10 * 2 ** i));
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -130,7 +130,47 @@ async function pruneOrphanedModelDownloads(stagingDir, maxAgeMs = 60 * 60 * 1e3)
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
async function removeStaleFileLock(lockPath, staleAgeMs) {
|
|
134
|
+
try {
|
|
135
|
+
const s = await fsPromises.stat(lockPath);
|
|
136
|
+
if (Date.now() - s.mtimeMs < staleAgeMs)
|
|
137
|
+
return;
|
|
138
|
+
await fsPromises.rmdir(lockPath);
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function acquireFileLock(filePath, timeoutMs = 250, staleAgeMs = 3e4) {
|
|
143
|
+
const lockPath = `${filePath}.lock`;
|
|
144
|
+
const deadline = Date.now() + timeoutMs;
|
|
145
|
+
while (true) {
|
|
146
|
+
try {
|
|
147
|
+
await fsPromises.mkdir(lockPath);
|
|
148
|
+
return async () => {
|
|
149
|
+
try {
|
|
150
|
+
await fsPromises.rmdir(lockPath);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
await removeStaleFileLock(lockPath, staleAgeMs);
|
|
156
|
+
const remainingMs = deadline - Date.now();
|
|
157
|
+
if (remainingMs <= 0)
|
|
158
|
+
return void 0;
|
|
159
|
+
await (0, import_promises.setTimeout)(Math.min(50, remainingMs));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function withFileLock(filePath, callback) {
|
|
164
|
+
const release = await acquireFileLock(filePath);
|
|
165
|
+
if (!release)
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
return await callback();
|
|
169
|
+
} finally {
|
|
170
|
+
await release();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
var import_node_crypto, fs, fsPromises, import_node_os, import_node_path, import_promises, name1, name2;
|
|
134
174
|
var init_file_utils = __esm({
|
|
135
175
|
"../core/dist/file-utils.js"() {
|
|
136
176
|
"use strict";
|
|
@@ -139,6 +179,7 @@ var init_file_utils = __esm({
|
|
|
139
179
|
fsPromises = __toESM(require("node:fs/promises"), 1);
|
|
140
180
|
import_node_os = require("node:os");
|
|
141
181
|
import_node_path = require("node:path");
|
|
182
|
+
import_promises = require("node:timers/promises");
|
|
142
183
|
name1 = "read";
|
|
143
184
|
name2 = "File";
|
|
144
185
|
}
|
|
@@ -4251,7 +4292,7 @@ var init_zod = __esm({
|
|
|
4251
4292
|
});
|
|
4252
4293
|
|
|
4253
4294
|
// ../core/dist/types.js
|
|
4254
|
-
var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
|
|
4295
|
+
var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, 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;
|
|
4255
4296
|
var init_types2 = __esm({
|
|
4256
4297
|
"../core/dist/types.js"() {
|
|
4257
4298
|
"use strict";
|
|
@@ -4275,7 +4316,8 @@ var init_types2 = __esm({
|
|
|
4275
4316
|
VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
|
|
4276
4317
|
ThreatSchema = external_exports.object({
|
|
4277
4318
|
id: external_exports.string(),
|
|
4278
|
-
version: external_exports.number().int().
|
|
4319
|
+
version: external_exports.number().int().positive(),
|
|
4320
|
+
detectionName: external_exports.string(),
|
|
4279
4321
|
category: external_exports.string(),
|
|
4280
4322
|
severity: VerdictSeveritySchema,
|
|
4281
4323
|
confidence: external_exports.number(),
|
|
@@ -4328,14 +4370,24 @@ var init_types2 = __esm({
|
|
|
4328
4370
|
AmsiCheckConfigSchema = external_exports.object({
|
|
4329
4371
|
enabled: external_exports.boolean().default(true)
|
|
4330
4372
|
});
|
|
4373
|
+
SkillCheckConfigSchema = external_exports.object({
|
|
4374
|
+
enabled: external_exports.boolean().default(true),
|
|
4375
|
+
cache_ttl_days: external_exports.number().min(0).default(1),
|
|
4376
|
+
/**
|
|
4377
|
+
* Upload unknown skills (never seen by the analyzer) for deep content
|
|
4378
|
+
* analysis. When false, the scan still looks skills up by content hash and
|
|
4379
|
+
* still flags known-risky ones — but no skill content ever leaves the
|
|
4380
|
+
* machine and the upload worker never runs (lookup-only mode).
|
|
4381
|
+
*/
|
|
4382
|
+
upload_enabled: external_exports.boolean().default(true)
|
|
4383
|
+
});
|
|
4331
4384
|
DEFAULT_PI_HIGH_RISK_THRESHOLD = 0.99;
|
|
4332
4385
|
DEFAULT_PI_MEDIUM_RISK_THRESHOLD = 0.5;
|
|
4386
|
+
DEFAULT_PI_TELEMETRY_THRESHOLD = 0.95;
|
|
4333
4387
|
PiCheckConfigSchema = external_exports.object({
|
|
4334
4388
|
enabled: external_exports.boolean().default(false),
|
|
4335
4389
|
max_content_length: external_exports.number().default(16384),
|
|
4336
|
-
model_path: external_exports.string().optional()
|
|
4337
|
-
high_risk_threshold: external_exports.number().default(DEFAULT_PI_HIGH_RISK_THRESHOLD),
|
|
4338
|
-
medium_risk_threshold: external_exports.number().default(DEFAULT_PI_MEDIUM_RISK_THRESHOLD)
|
|
4390
|
+
model_path: external_exports.string().optional()
|
|
4339
4391
|
});
|
|
4340
4392
|
ExceptionDecisionSchema = external_exports.enum(["allow", "deny"]);
|
|
4341
4393
|
ExceptionMatchSchema = external_exports.enum(["executable", "domain", "path", "plugin", "regex"]);
|
|
@@ -4357,6 +4409,7 @@ var init_types2 = __esm({
|
|
|
4357
4409
|
file_check: FileCheckConfigSchema.default({}),
|
|
4358
4410
|
package_check: PackageCheckConfigSchema.default({}),
|
|
4359
4411
|
amsi_check: AmsiCheckConfigSchema.default({}),
|
|
4412
|
+
skill_check: SkillCheckConfigSchema.default({}),
|
|
4360
4413
|
pi_check: PiCheckConfigSchema.default({}),
|
|
4361
4414
|
heuristics_enabled: external_exports.boolean().default(true),
|
|
4362
4415
|
cache: CacheConfigSchema.default({}),
|
|
@@ -4365,6 +4418,8 @@ var init_types2 = __esm({
|
|
|
4365
4418
|
operational_logging: OperationalLoggingConfigSchema.default({}),
|
|
4366
4419
|
sensitivity: SensitivitySchema.default("balanced"),
|
|
4367
4420
|
disabled_threats: external_exports.array(external_exports.string()).default([]),
|
|
4421
|
+
announce_clean_scans: external_exports.boolean().default(true),
|
|
4422
|
+
manage_status_line: external_exports.boolean().default(true),
|
|
4368
4423
|
brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
|
|
4369
4424
|
community_iq: external_exports.boolean().default(true)
|
|
4370
4425
|
});
|
|
@@ -4383,13 +4438,27 @@ var init_types2 = __esm({
|
|
|
4383
4438
|
var config_exports = {};
|
|
4384
4439
|
__export(config_exports, {
|
|
4385
4440
|
HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
|
|
4441
|
+
MS_PER_DAY: () => MS_PER_DAY,
|
|
4386
4442
|
SAGE_DIR: () => SAGE_DIR,
|
|
4387
4443
|
defaultConfigPath: () => defaultConfigPath,
|
|
4388
4444
|
getClaudeConfigDir: () => getClaudeConfigDir,
|
|
4445
|
+
isFreshTimestamp: () => isFreshTimestamp,
|
|
4389
4446
|
loadConfig: () => loadConfig,
|
|
4390
4447
|
loadConfigSync: () => loadConfigSync,
|
|
4391
|
-
|
|
4448
|
+
readExplicitSkillUploadEnabled: () => readExplicitSkillUploadEnabled,
|
|
4449
|
+
resolvePath: () => resolvePath,
|
|
4450
|
+
skillCacheTtlMs: () => skillCacheTtlMs
|
|
4392
4451
|
});
|
|
4452
|
+
function skillCacheTtlMs(config2) {
|
|
4453
|
+
return Math.max(1, config2.skill_check.cache_ttl_days) * MS_PER_DAY;
|
|
4454
|
+
}
|
|
4455
|
+
function isFreshTimestamp(ts, ttlMs, now = Date.now()) {
|
|
4456
|
+
if (Number.isNaN(ts))
|
|
4457
|
+
return false;
|
|
4458
|
+
if (ts - now > CLOCK_SKEW_TOLERANCE_MS)
|
|
4459
|
+
return false;
|
|
4460
|
+
return now - ts < ttlMs;
|
|
4461
|
+
}
|
|
4393
4462
|
function resolvedSageDir() {
|
|
4394
4463
|
return resolvePath(SAGE_DIR);
|
|
4395
4464
|
}
|
|
@@ -4464,7 +4533,7 @@ function sanitizeBrandKey(data, logger) {
|
|
|
4464
4533
|
if (typeof brandKey === "string" && brandKey.length >= 1 && brandKey.length <= 32 && BRAND_KEY_RE.test(brandKey)) {
|
|
4465
4534
|
return data;
|
|
4466
4535
|
}
|
|
4467
|
-
logger.warn(`Invalid brand_key in config
|
|
4536
|
+
logger.warn(`Invalid brand_key in config - ignoring`, { brand_key: brandKey });
|
|
4468
4537
|
const { brand_key: _, ...rest } = data;
|
|
4469
4538
|
return rest;
|
|
4470
4539
|
}
|
|
@@ -4516,6 +4585,21 @@ function parseConfig(raw, path, logger) {
|
|
|
4516
4585
|
return defaultConfig(logger);
|
|
4517
4586
|
}
|
|
4518
4587
|
}
|
|
4588
|
+
async function readExplicitSkillUploadEnabled(configPath, logger = nullLogger) {
|
|
4589
|
+
const path = configPath ? resolvePath(configPath) : defaultConfigPath();
|
|
4590
|
+
try {
|
|
4591
|
+
const data = JSON.parse(await getFileContent(path));
|
|
4592
|
+
const skillCheck = data.skill_check;
|
|
4593
|
+
if (skillCheck && typeof skillCheck === "object" && !Array.isArray(skillCheck) && "upload_enabled" in skillCheck) {
|
|
4594
|
+
const value = skillCheck.upload_enabled;
|
|
4595
|
+
if (typeof value === "boolean")
|
|
4596
|
+
return { present: true, value };
|
|
4597
|
+
logger.warn("Config skill_check.upload_enabled is not a boolean; ignoring", { value });
|
|
4598
|
+
}
|
|
4599
|
+
} catch {
|
|
4600
|
+
}
|
|
4601
|
+
return { present: false, value: false };
|
|
4602
|
+
}
|
|
4519
4603
|
async function loadConfig(configPath, logger = nullLogger) {
|
|
4520
4604
|
const path = configPath ? resolvePath(configPath) : defaultConfigPath();
|
|
4521
4605
|
try {
|
|
@@ -4532,7 +4616,7 @@ function loadConfigSync(configPath, logger = nullLogger) {
|
|
|
4532
4616
|
return defaultConfig(logger);
|
|
4533
4617
|
}
|
|
4534
4618
|
}
|
|
4535
|
-
var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, BRAND_KEY_RE;
|
|
4619
|
+
var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
|
|
4536
4620
|
var init_config = __esm({
|
|
4537
4621
|
"../core/dist/config.js"() {
|
|
4538
4622
|
"use strict";
|
|
@@ -4541,6 +4625,8 @@ var init_config = __esm({
|
|
|
4541
4625
|
init_types2();
|
|
4542
4626
|
SAGE_DIR = "~/.sage";
|
|
4543
4627
|
HOOK_TIMEOUT_SECONDS = 8;
|
|
4628
|
+
MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
4629
|
+
CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1e3;
|
|
4544
4630
|
BRAND_KEY_RE = /^[a-z0-9_-]+$/u;
|
|
4545
4631
|
}
|
|
4546
4632
|
});
|
|
@@ -6805,7 +6891,7 @@ async function ensurePiDeps(modelPath, logger = nullLogger) {
|
|
|
6805
6891
|
}
|
|
6806
6892
|
}
|
|
6807
6893
|
function runNpm(cwd, args, logger) {
|
|
6808
|
-
return new Promise((
|
|
6894
|
+
return new Promise((resolve10, reject) => {
|
|
6809
6895
|
const isWindows = process.platform === "win32";
|
|
6810
6896
|
const npmCmd = isWindows ? "npm.cmd" : "npm";
|
|
6811
6897
|
const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
|
|
@@ -6813,7 +6899,7 @@ function runNpm(cwd, args, logger) {
|
|
|
6813
6899
|
logger.warn(`npm install error: ${stderr || error.message}`);
|
|
6814
6900
|
reject(error);
|
|
6815
6901
|
} else {
|
|
6816
|
-
|
|
6902
|
+
resolve10();
|
|
6817
6903
|
}
|
|
6818
6904
|
});
|
|
6819
6905
|
child.unref();
|
|
@@ -14139,7 +14225,7 @@ function resolveBranding(brandKey, logger) {
|
|
|
14139
14225
|
return defaultBranding;
|
|
14140
14226
|
const entry = BRANDS[brandKey];
|
|
14141
14227
|
if (!entry) {
|
|
14142
|
-
logger?.warn(`Unknown brand_key "${brandKey}" in config
|
|
14228
|
+
logger?.warn(`Unknown brand_key "${brandKey}" in config - using default branding`);
|
|
14143
14229
|
return defaultBranding;
|
|
14144
14230
|
}
|
|
14145
14231
|
return { ...entry, brand_key: brandKey };
|
|
@@ -14239,88 +14325,226 @@ var ApprovalStore = class {
|
|
|
14239
14325
|
// ../core/dist/audit-log.js
|
|
14240
14326
|
var import_node_crypto3 = require("node:crypto");
|
|
14241
14327
|
init_config();
|
|
14328
|
+
|
|
14329
|
+
// ../core/dist/content-snapshot.js
|
|
14330
|
+
var import_node_os2 = require("node:os");
|
|
14331
|
+
var CONTENT_FIELD_LIMITS = Object.freeze({
|
|
14332
|
+
command: 512,
|
|
14333
|
+
url: 512,
|
|
14334
|
+
file_path: 512,
|
|
14335
|
+
package_name: 256,
|
|
14336
|
+
package_version: 128,
|
|
14337
|
+
package_registry: 128
|
|
14338
|
+
});
|
|
14339
|
+
function safeTruncate(value, maxLen) {
|
|
14340
|
+
if (maxLen <= 0)
|
|
14341
|
+
return "";
|
|
14342
|
+
if (value.length <= maxLen)
|
|
14343
|
+
return value;
|
|
14344
|
+
const cutIndex = maxLen;
|
|
14345
|
+
const codeUnit = value.charCodeAt(cutIndex - 1);
|
|
14346
|
+
if (codeUnit >= 55296 && codeUnit <= 56319) {
|
|
14347
|
+
return value.slice(0, cutIndex - 1);
|
|
14348
|
+
}
|
|
14349
|
+
return value.slice(0, cutIndex);
|
|
14350
|
+
}
|
|
14351
|
+
function scrubHomePath(value) {
|
|
14352
|
+
const home = (0, import_node_os2.homedir)();
|
|
14353
|
+
if (!home)
|
|
14354
|
+
return value;
|
|
14355
|
+
const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
14356
|
+
if (!normalizedHome)
|
|
14357
|
+
return value;
|
|
14358
|
+
const normalizedValue = value.replace(/\\/g, "/");
|
|
14359
|
+
if (normalizedValue === normalizedHome)
|
|
14360
|
+
return "~";
|
|
14361
|
+
if (normalizedValue.startsWith(`${normalizedHome}/`)) {
|
|
14362
|
+
return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
|
|
14363
|
+
}
|
|
14364
|
+
return value;
|
|
14365
|
+
}
|
|
14366
|
+
function asString(v) {
|
|
14367
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
14368
|
+
}
|
|
14369
|
+
function resolveFilePath(toolInput) {
|
|
14370
|
+
return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
|
|
14371
|
+
}
|
|
14372
|
+
function resolveWebFetchUrl(toolInput) {
|
|
14373
|
+
const single = asString(toolInput.url);
|
|
14374
|
+
if (single)
|
|
14375
|
+
return single;
|
|
14376
|
+
const urls = toolInput.urls;
|
|
14377
|
+
if (Array.isArray(urls)) {
|
|
14378
|
+
for (const u of urls) {
|
|
14379
|
+
if (typeof u === "string" && u.length > 0)
|
|
14380
|
+
return u;
|
|
14381
|
+
}
|
|
14382
|
+
}
|
|
14383
|
+
return void 0;
|
|
14384
|
+
}
|
|
14385
|
+
function extractFirstApplyPatchPath(patchText) {
|
|
14386
|
+
if (!patchText)
|
|
14387
|
+
return void 0;
|
|
14388
|
+
const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
|
|
14389
|
+
if (headerMatch?.[1]) {
|
|
14390
|
+
const trimmed = headerMatch[1].trim();
|
|
14391
|
+
if (trimmed)
|
|
14392
|
+
return trimmed;
|
|
14393
|
+
}
|
|
14394
|
+
const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
|
|
14395
|
+
if (renameMatch?.[1]) {
|
|
14396
|
+
const raw = renameMatch[1].trim();
|
|
14397
|
+
const arrow = raw.indexOf(" -> ");
|
|
14398
|
+
if (arrow !== -1) {
|
|
14399
|
+
const src = raw.slice(0, arrow).trim();
|
|
14400
|
+
if (src)
|
|
14401
|
+
return src;
|
|
14402
|
+
const dst = raw.slice(arrow + 4).trim();
|
|
14403
|
+
if (dst)
|
|
14404
|
+
return dst;
|
|
14405
|
+
} else if (raw) {
|
|
14406
|
+
return raw;
|
|
14407
|
+
}
|
|
14408
|
+
}
|
|
14409
|
+
return void 0;
|
|
14410
|
+
}
|
|
14411
|
+
function extractMcpContent(toolInput) {
|
|
14412
|
+
const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
|
|
14413
|
+
const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
|
|
14414
|
+
return {
|
|
14415
|
+
command: asString(candidate.command),
|
|
14416
|
+
url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
|
|
14417
|
+
file_path: resolveFilePath(candidate)
|
|
14418
|
+
};
|
|
14419
|
+
}
|
|
14420
|
+
function firstUrlArtifact(artifacts) {
|
|
14421
|
+
for (const a of artifacts) {
|
|
14422
|
+
if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
|
|
14423
|
+
return a.value;
|
|
14424
|
+
}
|
|
14425
|
+
}
|
|
14426
|
+
return void 0;
|
|
14427
|
+
}
|
|
14428
|
+
function applyFieldLimits(content) {
|
|
14429
|
+
for (const [key, value] of Object.entries(content)) {
|
|
14430
|
+
if (typeof value !== "string")
|
|
14431
|
+
continue;
|
|
14432
|
+
let sanitized = value;
|
|
14433
|
+
if (key === "file_path" || key === "command") {
|
|
14434
|
+
sanitized = scrubHomePath(sanitized);
|
|
14435
|
+
}
|
|
14436
|
+
const limit = CONTENT_FIELD_LIMITS[key];
|
|
14437
|
+
if (limit && sanitized.length > limit) {
|
|
14438
|
+
sanitized = safeTruncate(sanitized, limit);
|
|
14439
|
+
}
|
|
14440
|
+
content[key] = sanitized;
|
|
14441
|
+
}
|
|
14442
|
+
}
|
|
14443
|
+
function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
|
|
14444
|
+
const content = {};
|
|
14445
|
+
switch (toolType) {
|
|
14446
|
+
case "Bash": {
|
|
14447
|
+
const command = asString(toolInput.command);
|
|
14448
|
+
if (command)
|
|
14449
|
+
content.command = command;
|
|
14450
|
+
break;
|
|
14451
|
+
}
|
|
14452
|
+
case "WebFetch": {
|
|
14453
|
+
const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
|
|
14454
|
+
if (url)
|
|
14455
|
+
content.url = url;
|
|
14456
|
+
break;
|
|
14457
|
+
}
|
|
14458
|
+
case "Write":
|
|
14459
|
+
case "Edit":
|
|
14460
|
+
case "Read":
|
|
14461
|
+
case "Delete": {
|
|
14462
|
+
const filePath = resolveFilePath(toolInput);
|
|
14463
|
+
if (filePath)
|
|
14464
|
+
content.file_path = filePath;
|
|
14465
|
+
break;
|
|
14466
|
+
}
|
|
14467
|
+
case "ApplyPatch": {
|
|
14468
|
+
const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
|
|
14469
|
+
const filePath = extractFirstApplyPatchPath(patchText);
|
|
14470
|
+
if (filePath)
|
|
14471
|
+
content.file_path = filePath;
|
|
14472
|
+
break;
|
|
14473
|
+
}
|
|
14474
|
+
case "MCP": {
|
|
14475
|
+
const mcp = extractMcpContent(toolInput);
|
|
14476
|
+
if (mcp.command)
|
|
14477
|
+
content.command = mcp.command;
|
|
14478
|
+
if (mcp.url)
|
|
14479
|
+
content.url = mcp.url;
|
|
14480
|
+
if (mcp.file_path)
|
|
14481
|
+
content.file_path = mcp.file_path;
|
|
14482
|
+
break;
|
|
14483
|
+
}
|
|
14484
|
+
}
|
|
14485
|
+
if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
|
|
14486
|
+
const firstUrl = signals.url_checks[0]?.url;
|
|
14487
|
+
if (firstUrl)
|
|
14488
|
+
content.url = firstUrl;
|
|
14489
|
+
}
|
|
14490
|
+
if (signals.package_checks && signals.package_checks.length > 0) {
|
|
14491
|
+
const p = signals.package_checks[0];
|
|
14492
|
+
if (p?.package_name)
|
|
14493
|
+
content.package_name = p.package_name;
|
|
14494
|
+
if (p?.package_version)
|
|
14495
|
+
content.package_version = p.package_version;
|
|
14496
|
+
if (p?.package_registry)
|
|
14497
|
+
content.package_registry = p.package_registry;
|
|
14498
|
+
}
|
|
14499
|
+
applyFieldLimits(content);
|
|
14500
|
+
return content;
|
|
14501
|
+
}
|
|
14502
|
+
|
|
14503
|
+
// ../core/dist/audit-log.js
|
|
14242
14504
|
init_file_utils();
|
|
14243
14505
|
|
|
14244
14506
|
// ../core/dist/jsonl-log-writer.js
|
|
14245
|
-
var
|
|
14507
|
+
var import_promises2 = require("node:fs/promises");
|
|
14246
14508
|
var import_node_path4 = require("node:path");
|
|
14247
|
-
var import_promises2 = require("node:timers/promises");
|
|
14248
14509
|
init_config();
|
|
14510
|
+
init_file_utils();
|
|
14249
14511
|
var writeQueues = /* @__PURE__ */ new Map();
|
|
14250
|
-
var ROTATE_LOCK_TIMEOUT_MS = 250;
|
|
14251
|
-
var ROTATE_LOCK_STALE_MS = 3e4;
|
|
14252
|
-
var ROTATE_LOCK_POLL_MS = 50;
|
|
14253
14512
|
async function shouldRotate(filePath, maxBytes, maxFiles) {
|
|
14254
14513
|
if (maxBytes <= 0 || maxFiles <= 0)
|
|
14255
14514
|
return false;
|
|
14256
14515
|
try {
|
|
14257
|
-
const s = await (0,
|
|
14516
|
+
const s = await (0, import_promises2.stat)(filePath);
|
|
14258
14517
|
return s.size >= maxBytes;
|
|
14259
14518
|
} catch {
|
|
14260
14519
|
return false;
|
|
14261
14520
|
}
|
|
14262
14521
|
}
|
|
14263
|
-
async function removeStaleRotateLock(lockPath) {
|
|
14264
|
-
try {
|
|
14265
|
-
const s = await (0, import_promises.stat)(lockPath);
|
|
14266
|
-
if (Date.now() - s.mtimeMs < ROTATE_LOCK_STALE_MS)
|
|
14267
|
-
return;
|
|
14268
|
-
await (0, import_promises.rmdir)(lockPath);
|
|
14269
|
-
} catch {
|
|
14270
|
-
}
|
|
14271
|
-
}
|
|
14272
|
-
async function acquireRotateLock(filePath) {
|
|
14273
|
-
const lockPath = `${filePath}.lock`;
|
|
14274
|
-
const deadline = Date.now() + ROTATE_LOCK_TIMEOUT_MS;
|
|
14275
|
-
while (true) {
|
|
14276
|
-
try {
|
|
14277
|
-
await (0, import_promises.mkdir)(lockPath);
|
|
14278
|
-
return async () => {
|
|
14279
|
-
try {
|
|
14280
|
-
await (0, import_promises.rmdir)(lockPath);
|
|
14281
|
-
} catch {
|
|
14282
|
-
}
|
|
14283
|
-
};
|
|
14284
|
-
} catch {
|
|
14285
|
-
await removeStaleRotateLock(lockPath);
|
|
14286
|
-
const remainingMs = deadline - Date.now();
|
|
14287
|
-
if (remainingMs <= 0)
|
|
14288
|
-
return void 0;
|
|
14289
|
-
await (0, import_promises2.setTimeout)(Math.min(ROTATE_LOCK_POLL_MS, remainingMs));
|
|
14290
|
-
}
|
|
14291
|
-
}
|
|
14292
|
-
}
|
|
14293
14522
|
async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
|
|
14294
14523
|
if (!await shouldRotate(filePath, maxBytes, maxFiles))
|
|
14295
14524
|
return;
|
|
14296
|
-
|
|
14297
|
-
if (!releaseLock)
|
|
14298
|
-
return;
|
|
14299
|
-
try {
|
|
14525
|
+
await withFileLock(filePath, async () => {
|
|
14300
14526
|
if (!await shouldRotate(filePath, maxBytes, maxFiles))
|
|
14301
14527
|
return;
|
|
14302
14528
|
try {
|
|
14303
|
-
await (0,
|
|
14529
|
+
await (0, import_promises2.unlink)(`${filePath}.${maxFiles}`);
|
|
14304
14530
|
} catch {
|
|
14305
14531
|
}
|
|
14306
14532
|
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
14307
14533
|
try {
|
|
14308
|
-
await (0,
|
|
14534
|
+
await (0, import_promises2.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
|
|
14309
14535
|
} catch {
|
|
14310
14536
|
}
|
|
14311
14537
|
}
|
|
14312
14538
|
try {
|
|
14313
|
-
await (0,
|
|
14539
|
+
await (0, import_promises2.rename)(filePath, `${filePath}.1`);
|
|
14314
14540
|
} catch {
|
|
14315
14541
|
}
|
|
14316
|
-
}
|
|
14317
|
-
await releaseLock();
|
|
14318
|
-
}
|
|
14542
|
+
});
|
|
14319
14543
|
}
|
|
14320
14544
|
async function appendJsonlEntryNow(path, config2, entry) {
|
|
14321
|
-
await (0,
|
|
14545
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
14322
14546
|
await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
|
|
14323
|
-
await (0,
|
|
14547
|
+
await (0, import_promises2.appendFile)(path, `${JSON.stringify(entry)}
|
|
14324
14548
|
`);
|
|
14325
14549
|
}
|
|
14326
14550
|
async function appendJsonlEntry(config2, entry) {
|
|
@@ -14338,6 +14562,7 @@ async function appendJsonlEntry(config2, entry) {
|
|
|
14338
14562
|
}
|
|
14339
14563
|
|
|
14340
14564
|
// ../core/dist/audit-log.js
|
|
14565
|
+
init_types2();
|
|
14341
14566
|
var MAX_SUMMARY_LEN = 200;
|
|
14342
14567
|
var AUDIT_LOG_SCHEMA_VERSION = 1;
|
|
14343
14568
|
function toolInputSummary(toolName, toolInput) {
|
|
@@ -14378,6 +14603,7 @@ async function logVerdict(config2, input) {
|
|
|
14378
14603
|
severity: input.verdict.severity,
|
|
14379
14604
|
reasons: input.verdict.reasons,
|
|
14380
14605
|
source: input.verdict.source,
|
|
14606
|
+
threat_id: input.verdict.matchedThreatId,
|
|
14381
14607
|
user_override: userOverride,
|
|
14382
14608
|
signals: input.signals,
|
|
14383
14609
|
tool_use_id: input.toolUseId
|
|
@@ -14385,6 +14611,9 @@ async function logVerdict(config2, input) {
|
|
|
14385
14611
|
if (input.content !== void 0) {
|
|
14386
14612
|
entry.content = input.content;
|
|
14387
14613
|
}
|
|
14614
|
+
if (input.contentSnippet !== void 0) {
|
|
14615
|
+
entry.content_snippet = input.contentSnippet;
|
|
14616
|
+
}
|
|
14388
14617
|
try {
|
|
14389
14618
|
await appendEntry(config2, entry);
|
|
14390
14619
|
} catch {
|
|
@@ -14406,6 +14635,22 @@ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
|
|
|
14406
14635
|
} catch {
|
|
14407
14636
|
}
|
|
14408
14637
|
}
|
|
14638
|
+
async function logSkillQueued(config2, skillId, pluginKey, skillFolder) {
|
|
14639
|
+
if (!config2.enabled)
|
|
14640
|
+
return;
|
|
14641
|
+
try {
|
|
14642
|
+
await appendEntry(config2, {
|
|
14643
|
+
type: "skill_queued",
|
|
14644
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14645
|
+
skill_id: skillId,
|
|
14646
|
+
plugin_key: pluginKey,
|
|
14647
|
+
// Home-scrubbed folder path: aids forensics when the same skill id shows
|
|
14648
|
+
// up under multiple plugins. Omitted when the caller has no path.
|
|
14649
|
+
...skillFolder ? { skill_folder: scrubHomePath(skillFolder) } : {}
|
|
14650
|
+
});
|
|
14651
|
+
} catch {
|
|
14652
|
+
}
|
|
14653
|
+
}
|
|
14409
14654
|
|
|
14410
14655
|
// ../core/dist/cache.js
|
|
14411
14656
|
init_config();
|
|
@@ -14844,19 +15089,19 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
14844
15089
|
return this.available;
|
|
14845
15090
|
}
|
|
14846
15091
|
waitForLine(timeout) {
|
|
14847
|
-
return new Promise((
|
|
15092
|
+
return new Promise((resolve10, reject) => {
|
|
14848
15093
|
const timer = setTimeout(() => {
|
|
14849
15094
|
this.pendingResponse = null;
|
|
14850
15095
|
reject(new Error("timeout"));
|
|
14851
15096
|
}, timeout);
|
|
14852
|
-
this.pendingResponse = { resolve:
|
|
15097
|
+
this.pendingResponse = { resolve: resolve10, reject, timer };
|
|
14853
15098
|
});
|
|
14854
15099
|
}
|
|
14855
15100
|
async enqueueScan(operation) {
|
|
14856
15101
|
const previous = this.scanQueue;
|
|
14857
15102
|
let release;
|
|
14858
|
-
this.scanQueue = new Promise((
|
|
14859
|
-
release =
|
|
15103
|
+
this.scanQueue = new Promise((resolve10) => {
|
|
15104
|
+
release = resolve10;
|
|
14860
15105
|
});
|
|
14861
15106
|
await previous;
|
|
14862
15107
|
try {
|
|
@@ -14904,10 +15149,10 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
14904
15149
|
const line = this.stdoutBuffer.slice(0, idx).trim();
|
|
14905
15150
|
this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
|
|
14906
15151
|
if (this.pendingResponse) {
|
|
14907
|
-
const { resolve:
|
|
15152
|
+
const { resolve: resolve10, timer } = this.pendingResponse;
|
|
14908
15153
|
this.pendingResponse = null;
|
|
14909
15154
|
clearTimeout(timer);
|
|
14910
|
-
|
|
15155
|
+
resolve10(line);
|
|
14911
15156
|
}
|
|
14912
15157
|
idx = this.stdoutBuffer.indexOf("\n");
|
|
14913
15158
|
}
|
|
@@ -15006,7 +15251,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15006
15251
|
if (!this.available)
|
|
15007
15252
|
return null;
|
|
15008
15253
|
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
15009
|
-
return new Promise((
|
|
15254
|
+
return new Promise((resolve10) => {
|
|
15010
15255
|
try {
|
|
15011
15256
|
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
|
|
15012
15257
|
let stdout = "";
|
|
@@ -15016,7 +15261,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15016
15261
|
ps.kill();
|
|
15017
15262
|
} catch {
|
|
15018
15263
|
}
|
|
15019
|
-
|
|
15264
|
+
resolve10(null);
|
|
15020
15265
|
}, PS_TIMEOUT);
|
|
15021
15266
|
ps.stdout?.on("data", (chunk) => {
|
|
15022
15267
|
stdout += chunk.toString();
|
|
@@ -15029,7 +15274,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15029
15274
|
ps.on("error", (err) => {
|
|
15030
15275
|
clearTimeout(timer);
|
|
15031
15276
|
this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
|
|
15032
|
-
|
|
15277
|
+
resolve10(null);
|
|
15033
15278
|
});
|
|
15034
15279
|
ps.on("exit", () => {
|
|
15035
15280
|
clearTimeout(timer);
|
|
@@ -15042,11 +15287,11 @@ var WslPowershellAmsiBackend = class {
|
|
|
15042
15287
|
contentName
|
|
15043
15288
|
});
|
|
15044
15289
|
}
|
|
15045
|
-
|
|
15290
|
+
resolve10(null);
|
|
15046
15291
|
return;
|
|
15047
15292
|
}
|
|
15048
15293
|
this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
|
|
15049
|
-
|
|
15294
|
+
resolve10(interpretAmsiResult(amsiResult, content, contentName));
|
|
15050
15295
|
});
|
|
15051
15296
|
const req = JSON.stringify({ content: truncated, contentName });
|
|
15052
15297
|
ps.stdin?.write(`${req}
|
|
@@ -15054,7 +15299,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15054
15299
|
ps.stdin?.end();
|
|
15055
15300
|
} catch (e) {
|
|
15056
15301
|
this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
|
|
15057
|
-
|
|
15302
|
+
resolve10(null);
|
|
15058
15303
|
}
|
|
15059
15304
|
});
|
|
15060
15305
|
}
|
|
@@ -15200,7 +15445,7 @@ init_file_utils();
|
|
|
15200
15445
|
var import_meta = {};
|
|
15201
15446
|
function resolveVersion() {
|
|
15202
15447
|
if (true)
|
|
15203
|
-
return "0.
|
|
15448
|
+
return "0.13.0";
|
|
15204
15449
|
try {
|
|
15205
15450
|
const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
|
|
15206
15451
|
const pkg = JSON.parse(getFileContentSync(pkgPath));
|
|
@@ -15349,9 +15594,10 @@ var FileCheckClient = class {
|
|
|
15349
15594
|
var import_node_fs = require("node:fs");
|
|
15350
15595
|
var import_node_path6 = require("node:path");
|
|
15351
15596
|
init_config();
|
|
15352
|
-
var MODEL_SCHEMA_VERSION = "
|
|
15597
|
+
var MODEL_SCHEMA_VERSION = "v2";
|
|
15353
15598
|
var REQUIRED_MODELS_BY_SCHEMA = {
|
|
15354
|
-
v1: ["pi-model"]
|
|
15599
|
+
v1: ["pi-model"],
|
|
15600
|
+
v2: ["pi-model"]
|
|
15355
15601
|
};
|
|
15356
15602
|
var REQUIRED_MODEL_FILES = {
|
|
15357
15603
|
"pi-model": [
|
|
@@ -15385,8 +15631,11 @@ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
|
15385
15631
|
init_types2();
|
|
15386
15632
|
var STALE_LOCK_MS = 60 * 60 * 1e3;
|
|
15387
15633
|
|
|
15634
|
+
// ../core/dist/clients/model-manifest.js
|
|
15635
|
+
init_config();
|
|
15636
|
+
|
|
15388
15637
|
// ../core/dist/sage-proxy.js
|
|
15389
|
-
function
|
|
15638
|
+
function mapSageHostOs(platform) {
|
|
15390
15639
|
switch (platform) {
|
|
15391
15640
|
case "win32":
|
|
15392
15641
|
return "WINDOWS";
|
|
@@ -15398,21 +15647,37 @@ function mapSageProxyOs(platform) {
|
|
|
15398
15647
|
return platform;
|
|
15399
15648
|
}
|
|
15400
15649
|
}
|
|
15401
|
-
function
|
|
15650
|
+
function mapSageHostArchitecture(arch) {
|
|
15402
15651
|
return arch.toUpperCase();
|
|
15403
15652
|
}
|
|
15404
|
-
function
|
|
15653
|
+
function buildSageUserConfig(config2) {
|
|
15405
15654
|
return {
|
|
15406
|
-
|
|
15655
|
+
sensitivity: config2.sensitivity,
|
|
15656
|
+
url_check_enabled: config2.url_check.enabled,
|
|
15657
|
+
file_check_enabled: config2.file_check.enabled,
|
|
15658
|
+
package_check_enabled: config2.package_check.enabled,
|
|
15659
|
+
heuristics_enabled: config2.heuristics_enabled,
|
|
15660
|
+
pi_check_enabled: config2.pi_check.enabled,
|
|
15661
|
+
community_iq_enabled: config2.community_iq,
|
|
15662
|
+
// Optional-chained + defaulted: real callers pass a loadConfig result (skill_check
|
|
15663
|
+
// always present via the schema default), but this feeds fail-open telemetry, so a
|
|
15664
|
+
// partial config must never crash the envelope build.
|
|
15665
|
+
skill_check_upload_enabled: config2.skill_check?.upload_enabled ?? true
|
|
15666
|
+
};
|
|
15667
|
+
}
|
|
15668
|
+
function buildSageProxyEnvelope(args) {
|
|
15669
|
+
return {
|
|
15670
|
+
identity: { uuid: args.iid },
|
|
15407
15671
|
product: { version_app: args.versionApp },
|
|
15408
15672
|
platform: {
|
|
15409
|
-
os: args.platformOs ??
|
|
15410
|
-
architecture: args.platformArchitecture ??
|
|
15673
|
+
os: args.platformOs ?? mapSageHostOs(process.platform),
|
|
15674
|
+
architecture: args.platformArchitecture ?? mapSageHostArchitecture(process.arch)
|
|
15411
15675
|
},
|
|
15412
15676
|
agent: {
|
|
15413
15677
|
agent_runtime: args.agentRuntime,
|
|
15414
15678
|
agent_runtime_version: args.agentRuntimeVersion
|
|
15415
|
-
}
|
|
15679
|
+
},
|
|
15680
|
+
...args.config ? { config: buildSageUserConfig(args.config) } : {}
|
|
15416
15681
|
};
|
|
15417
15682
|
}
|
|
15418
15683
|
|
|
@@ -15766,14 +16031,29 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
15766
16031
|
if (truncated.length === 0)
|
|
15767
16032
|
return null;
|
|
15768
16033
|
const loaded = await this.ensureLoaded();
|
|
15769
|
-
if (!loaded)
|
|
16034
|
+
if (!loaded) {
|
|
16035
|
+
this.logger.info("PI model inference skipped", {
|
|
16036
|
+
context,
|
|
16037
|
+
reason: "model_unavailable"
|
|
16038
|
+
});
|
|
15770
16039
|
return null;
|
|
16040
|
+
}
|
|
15771
16041
|
const chunks = this.chunkText(truncated);
|
|
16042
|
+
const modelId = (0, import_node_path9.basename)(this.modelPath);
|
|
16043
|
+
this.logger.info("PI model inference started", {
|
|
16044
|
+
context,
|
|
16045
|
+
modelId,
|
|
16046
|
+
contentLength: content.length,
|
|
16047
|
+
truncatedContentLength: truncated.length,
|
|
16048
|
+
chunks: chunks.length
|
|
16049
|
+
});
|
|
15772
16050
|
let maxRisk = 0;
|
|
15773
16051
|
let maxChunk = "";
|
|
16052
|
+
let chunksScanned = 0;
|
|
15774
16053
|
for (const chunk of chunks) {
|
|
15775
16054
|
if (chunk.length < 10)
|
|
15776
16055
|
continue;
|
|
16056
|
+
chunksScanned++;
|
|
15777
16057
|
const risk = await this.classifyChunk(chunk);
|
|
15778
16058
|
if (risk > maxRisk) {
|
|
15779
16059
|
maxRisk = risk;
|
|
@@ -15785,13 +16065,21 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
15785
16065
|
const snippet = maxChunk.length > 80 ? `${maxChunk.slice(0, 77)}...` : maxChunk;
|
|
15786
16066
|
findings.push(snippet);
|
|
15787
16067
|
}
|
|
15788
|
-
|
|
16068
|
+
const result = {
|
|
15789
16069
|
risk: maxRisk,
|
|
15790
16070
|
findings,
|
|
15791
16071
|
contentName: context,
|
|
15792
|
-
modelId
|
|
16072
|
+
modelId,
|
|
15793
16073
|
contentSnippet: maxChunk || void 0
|
|
15794
16074
|
};
|
|
16075
|
+
this.logger.info("PI model inference completed", {
|
|
16076
|
+
context,
|
|
16077
|
+
modelId,
|
|
16078
|
+
risk: result.risk,
|
|
16079
|
+
findingsCount: result.findings.length,
|
|
16080
|
+
chunksScanned
|
|
16081
|
+
});
|
|
16082
|
+
return result;
|
|
15795
16083
|
} catch (err) {
|
|
15796
16084
|
this.logger.warn("PI check failed (fail-open)", {
|
|
15797
16085
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -15920,6 +16208,9 @@ ${content.slice(-tailLen)}`;
|
|
|
15920
16208
|
// ../core/dist/index.js
|
|
15921
16209
|
init_pi_deps_installer();
|
|
15922
16210
|
|
|
16211
|
+
// ../core/dist/clients/skill-analyze.js
|
|
16212
|
+
init_types2();
|
|
16213
|
+
|
|
15923
16214
|
// ../core/dist/clients/skill-check.js
|
|
15924
16215
|
init_types2();
|
|
15925
16216
|
var DEFAULT_TIMEOUT4 = 5;
|
|
@@ -15942,9 +16233,13 @@ var SkillCheckClient = class {
|
|
|
15942
16233
|
/**
|
|
15943
16234
|
* Check a list of skill IDs against the proxy.
|
|
15944
16235
|
*
|
|
15945
|
-
* Returns a Map keyed by skill id
|
|
15946
|
-
*
|
|
15947
|
-
*
|
|
16236
|
+
* Returns a Map keyed by skill id:
|
|
16237
|
+
* - `null` — proxy had no opinion (explicit null in response, or skill absent
|
|
16238
|
+
* from an otherwise-successful response). Callers should treat this as
|
|
16239
|
+
* "unknown, queue for upload".
|
|
16240
|
+
* - absent — the entire batch failed (HTTP error or network error). Callers
|
|
16241
|
+
* should fail open and skip the skill rather than enqueue it.
|
|
16242
|
+
* - object — a verdict from the analyzer.
|
|
15948
16243
|
*
|
|
15949
16244
|
* Fails-open: on any error returns an empty Map. Per-batch errors do
|
|
15950
16245
|
* not poison results from successful batches.
|
|
@@ -15988,8 +16283,10 @@ var SkillCheckClient = class {
|
|
|
15988
16283
|
const data = await response.json();
|
|
15989
16284
|
const results = data.results ?? {};
|
|
15990
16285
|
for (const id of skillIds) {
|
|
15991
|
-
if (!(id in results))
|
|
16286
|
+
if (!(id in results)) {
|
|
16287
|
+
out.set(id, null);
|
|
15992
16288
|
continue;
|
|
16289
|
+
}
|
|
15993
16290
|
const raw = results[id];
|
|
15994
16291
|
if (raw === null || raw === void 0) {
|
|
15995
16292
|
out.set(id, null);
|
|
@@ -16004,17 +16301,10 @@ var SkillCheckClient = class {
|
|
|
16004
16301
|
return out;
|
|
16005
16302
|
}
|
|
16006
16303
|
parseResult(skillId, raw) {
|
|
16007
|
-
const recommendationsRaw = raw.recommendations ?? [];
|
|
16008
|
-
const recommendations = recommendationsRaw.filter((r) => typeof r === "string");
|
|
16009
|
-
const categoriesRaw = raw.threat_categories ?? [];
|
|
16010
|
-
const threatCategories = categoriesRaw.filter((c) => typeof c === "string");
|
|
16011
16304
|
return {
|
|
16012
16305
|
skillId,
|
|
16013
16306
|
verdict: typeof raw.verdict === "string" ? raw.verdict : void 0,
|
|
16014
|
-
|
|
16015
|
-
summary: typeof raw.summary === "string" ? raw.summary : void 0,
|
|
16016
|
-
recommendations,
|
|
16017
|
-
threatCategories
|
|
16307
|
+
summary: typeof raw.summary === "string" ? raw.summary : void 0
|
|
16018
16308
|
};
|
|
16019
16309
|
}
|
|
16020
16310
|
};
|
|
@@ -16022,6 +16312,70 @@ var SkillCheckClient = class {
|
|
|
16022
16312
|
// ../core/dist/index.js
|
|
16023
16313
|
init_config();
|
|
16024
16314
|
|
|
16315
|
+
// ../core/dist/config-defaults.js
|
|
16316
|
+
var import_promises3 = require("node:fs/promises");
|
|
16317
|
+
var import_node_path10 = require("node:path");
|
|
16318
|
+
init_file_utils();
|
|
16319
|
+
init_types2();
|
|
16320
|
+
var CONFIG_DEFAULTS_FILENAME = "config.default.json";
|
|
16321
|
+
var CONFIG_DEFAULTS_SCHEMA_VERSION = 2;
|
|
16322
|
+
function buildConfigDefaults() {
|
|
16323
|
+
return {
|
|
16324
|
+
schema_version: CONFIG_DEFAULTS_SCHEMA_VERSION,
|
|
16325
|
+
...ConfigSchema.parse({})
|
|
16326
|
+
};
|
|
16327
|
+
}
|
|
16328
|
+
function serializeConfigDefaults() {
|
|
16329
|
+
return `${JSON.stringify(buildConfigDefaults(), null, 2)}
|
|
16330
|
+
`;
|
|
16331
|
+
}
|
|
16332
|
+
function readSchemaVersion(raw) {
|
|
16333
|
+
try {
|
|
16334
|
+
const data = JSON.parse(raw);
|
|
16335
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
16336
|
+
return null;
|
|
16337
|
+
const version = data.schema_version;
|
|
16338
|
+
return typeof version === "number" && Number.isInteger(version) ? version : null;
|
|
16339
|
+
} catch {
|
|
16340
|
+
return null;
|
|
16341
|
+
}
|
|
16342
|
+
}
|
|
16343
|
+
async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
|
|
16344
|
+
const targetPath = (0, import_node_path10.join)(sageDirPath, CONFIG_DEFAULTS_FILENAME);
|
|
16345
|
+
const defaults = serializeConfigDefaults();
|
|
16346
|
+
try {
|
|
16347
|
+
await (0, import_promises3.mkdir)(sageDirPath, { recursive: true, mode: 448 });
|
|
16348
|
+
await (0, import_promises3.chmod)(sageDirPath, 448);
|
|
16349
|
+
let existing = null;
|
|
16350
|
+
try {
|
|
16351
|
+
existing = await (0, import_promises3.readFile)(targetPath, "utf-8");
|
|
16352
|
+
} catch {
|
|
16353
|
+
}
|
|
16354
|
+
if (existing) {
|
|
16355
|
+
const existingVersion = readSchemaVersion(existing);
|
|
16356
|
+
if (existingVersion !== null && existingVersion > CONFIG_DEFAULTS_SCHEMA_VERSION) {
|
|
16357
|
+
logger.warn("Deployed config defaults are newer, update your installation", {
|
|
16358
|
+
path: targetPath,
|
|
16359
|
+
existingVersion,
|
|
16360
|
+
supportedVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
|
|
16361
|
+
});
|
|
16362
|
+
return;
|
|
16363
|
+
}
|
|
16364
|
+
if (existing === defaults)
|
|
16365
|
+
return;
|
|
16366
|
+
}
|
|
16367
|
+
await withFileLock(targetPath, async () => {
|
|
16368
|
+
await (0, import_promises3.writeFile)(targetPath, defaults, { encoding: "utf-8", mode: 384 });
|
|
16369
|
+
logger.debug("Deployed config defaults", {
|
|
16370
|
+
path: targetPath,
|
|
16371
|
+
schemaVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
|
|
16372
|
+
});
|
|
16373
|
+
});
|
|
16374
|
+
} catch (err) {
|
|
16375
|
+
logger.debug("Failed to deploy config defaults", { path: targetPath, error: String(err) });
|
|
16376
|
+
}
|
|
16377
|
+
}
|
|
16378
|
+
|
|
16025
16379
|
// ../core/dist/config-diagnostics.js
|
|
16026
16380
|
init_config();
|
|
16027
16381
|
init_file_utils();
|
|
@@ -16158,184 +16512,60 @@ function isScannableContent(content) {
|
|
|
16158
16512
|
return SNIFF_HTML.test(head) || SNIFF_HTML_TAGS.test(head) || SNIFF_JSON.test(head) || SNIFF_XML.test(head) || SNIFF_YAML.test(head) || SNIFF_SHEBANG_SHELL.test(head) || SNIFF_SHEBANG_PYTHON.test(head) || SNIFF_PYTHON.test(head) || SNIFF_JS_TS.test(head) || SNIFF_C_CPP.test(head) || SNIFF_KOTLIN.test(head) || SNIFF_CSV.test(head);
|
|
16159
16513
|
}
|
|
16160
16514
|
|
|
16161
|
-
// ../core/dist/
|
|
16162
|
-
var
|
|
16163
|
-
var
|
|
16164
|
-
|
|
16165
|
-
|
|
16166
|
-
|
|
16167
|
-
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
return "";
|
|
16174
|
-
if (value.length <= maxLen)
|
|
16175
|
-
return value;
|
|
16176
|
-
const cutIndex = maxLen;
|
|
16177
|
-
const codeUnit = value.charCodeAt(cutIndex - 1);
|
|
16178
|
-
if (codeUnit >= 55296 && codeUnit <= 56319) {
|
|
16179
|
-
return value.slice(0, cutIndex - 1);
|
|
16180
|
-
}
|
|
16181
|
-
return value.slice(0, cutIndex);
|
|
16182
|
-
}
|
|
16183
|
-
function scrubHomePath(value) {
|
|
16184
|
-
const home = (0, import_node_os2.homedir)();
|
|
16185
|
-
if (!home)
|
|
16186
|
-
return value;
|
|
16187
|
-
const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
16188
|
-
if (!normalizedHome)
|
|
16189
|
-
return value;
|
|
16190
|
-
const normalizedValue = value.replace(/\\/g, "/");
|
|
16191
|
-
if (normalizedValue === normalizedHome)
|
|
16192
|
-
return "~";
|
|
16193
|
-
if (normalizedValue.startsWith(`${normalizedHome}/`)) {
|
|
16194
|
-
return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
|
|
16195
|
-
}
|
|
16196
|
-
return value;
|
|
16197
|
-
}
|
|
16198
|
-
function asString(v) {
|
|
16199
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
16200
|
-
}
|
|
16201
|
-
function resolveFilePath(toolInput) {
|
|
16202
|
-
return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
|
|
16203
|
-
}
|
|
16204
|
-
function resolveWebFetchUrl(toolInput) {
|
|
16205
|
-
const single = asString(toolInput.url);
|
|
16206
|
-
if (single)
|
|
16207
|
-
return single;
|
|
16208
|
-
const urls = toolInput.urls;
|
|
16209
|
-
if (Array.isArray(urls)) {
|
|
16210
|
-
for (const u of urls) {
|
|
16211
|
-
if (typeof u === "string" && u.length > 0)
|
|
16212
|
-
return u;
|
|
16213
|
-
}
|
|
16214
|
-
}
|
|
16215
|
-
return void 0;
|
|
16515
|
+
// ../core/dist/detection-names.js
|
|
16516
|
+
var PI_DETECTION_NAME = "Other:SagePromptInjectionML-A [Susp]";
|
|
16517
|
+
var REPORTING_ENGINE_SHORTHANDS = {
|
|
16518
|
+
package: "sgpk",
|
|
16519
|
+
amsi: "sgam",
|
|
16520
|
+
heuristics: "sghe",
|
|
16521
|
+
ml: "sgml"
|
|
16522
|
+
};
|
|
16523
|
+
var REPORTING_DETECTION_SUFFIX = "sage";
|
|
16524
|
+
function formatReportingDetectionName(canonicalName, engine, optionalData = "") {
|
|
16525
|
+
const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
|
|
16526
|
+
return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
|
|
16216
16527
|
}
|
|
16217
|
-
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16226
|
-
|
|
16227
|
-
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
if (dst)
|
|
16236
|
-
return dst;
|
|
16237
|
-
} else if (raw) {
|
|
16238
|
-
return raw;
|
|
16239
|
-
}
|
|
16240
|
-
}
|
|
16241
|
-
return void 0;
|
|
16528
|
+
var PACKAGE_DETECTION_NAMES = {
|
|
16529
|
+
not_found: "Other:SagePackageNotFound-A [Susp]",
|
|
16530
|
+
suspicious_age: "Other:SagePackageNew-A [Susp]",
|
|
16531
|
+
malicious: "Other:SagePackageMalicious-A [Trj]",
|
|
16532
|
+
unknown: "Other:SagePackageUnknown-A [Susp]"
|
|
16533
|
+
};
|
|
16534
|
+
var AMSI_DETECTION_NAMES = {
|
|
16535
|
+
detected: "Other:SageAmsiDetected-A [Heur]",
|
|
16536
|
+
blocked_by_admin: "Other:SageAmsiBlockedByAdmin-A [Heur]",
|
|
16537
|
+
unknown: "Other:SageAmsiUnknown-A [Susp]"
|
|
16538
|
+
};
|
|
16539
|
+
var AMSI_REPORTING_RULE_NAMES = {
|
|
16540
|
+
detected: "AMSI_DETECTED",
|
|
16541
|
+
blocked_by_admin: "AMSI_BLOCKED_BY_ADMIN",
|
|
16542
|
+
unknown: "AMSI_UNKNOWN"
|
|
16543
|
+
};
|
|
16544
|
+
function isCanonicalDetectionName(name) {
|
|
16545
|
+
return /^\w+:\w+-[A-Z]+ \[[^\]\r\n]+\]$/.test(name);
|
|
16242
16546
|
}
|
|
16243
|
-
function
|
|
16244
|
-
|
|
16245
|
-
const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
|
|
16246
|
-
return {
|
|
16247
|
-
command: asString(candidate.command),
|
|
16248
|
-
url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
|
|
16249
|
-
file_path: resolveFilePath(candidate)
|
|
16250
|
-
};
|
|
16547
|
+
function packageDetectionName(verdict) {
|
|
16548
|
+
return PACKAGE_DETECTION_NAMES[verdict];
|
|
16251
16549
|
}
|
|
16252
|
-
function
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
return void 0;
|
|
16550
|
+
function amsiDetectionClass(amsiResult) {
|
|
16551
|
+
if (amsiResult >= 32768)
|
|
16552
|
+
return "detected";
|
|
16553
|
+
if (amsiResult >= 16384)
|
|
16554
|
+
return "blocked_by_admin";
|
|
16555
|
+
return "unknown";
|
|
16259
16556
|
}
|
|
16260
|
-
function
|
|
16261
|
-
|
|
16262
|
-
if (typeof value !== "string")
|
|
16263
|
-
continue;
|
|
16264
|
-
let sanitized = value;
|
|
16265
|
-
if (key === "file_path" || key === "command") {
|
|
16266
|
-
sanitized = scrubHomePath(sanitized);
|
|
16267
|
-
}
|
|
16268
|
-
const limit = CONTENT_FIELD_LIMITS[key];
|
|
16269
|
-
if (limit && sanitized.length > limit) {
|
|
16270
|
-
sanitized = safeTruncate(sanitized, limit);
|
|
16271
|
-
}
|
|
16272
|
-
content[key] = sanitized;
|
|
16273
|
-
}
|
|
16557
|
+
function amsiDetectionName(amsiResult) {
|
|
16558
|
+
return AMSI_DETECTION_NAMES[amsiDetectionClass(amsiResult)];
|
|
16274
16559
|
}
|
|
16275
|
-
function
|
|
16276
|
-
const
|
|
16277
|
-
|
|
16278
|
-
case "Bash": {
|
|
16279
|
-
const command = asString(toolInput.command);
|
|
16280
|
-
if (command)
|
|
16281
|
-
content.command = command;
|
|
16282
|
-
break;
|
|
16283
|
-
}
|
|
16284
|
-
case "WebFetch": {
|
|
16285
|
-
const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
|
|
16286
|
-
if (url)
|
|
16287
|
-
content.url = url;
|
|
16288
|
-
break;
|
|
16289
|
-
}
|
|
16290
|
-
case "Write":
|
|
16291
|
-
case "Edit":
|
|
16292
|
-
case "Read":
|
|
16293
|
-
case "Delete": {
|
|
16294
|
-
const filePath = resolveFilePath(toolInput);
|
|
16295
|
-
if (filePath)
|
|
16296
|
-
content.file_path = filePath;
|
|
16297
|
-
break;
|
|
16298
|
-
}
|
|
16299
|
-
case "ApplyPatch": {
|
|
16300
|
-
const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
|
|
16301
|
-
const filePath = extractFirstApplyPatchPath(patchText);
|
|
16302
|
-
if (filePath)
|
|
16303
|
-
content.file_path = filePath;
|
|
16304
|
-
break;
|
|
16305
|
-
}
|
|
16306
|
-
case "MCP": {
|
|
16307
|
-
const mcp = extractMcpContent(toolInput);
|
|
16308
|
-
if (mcp.command)
|
|
16309
|
-
content.command = mcp.command;
|
|
16310
|
-
if (mcp.url)
|
|
16311
|
-
content.url = mcp.url;
|
|
16312
|
-
if (mcp.file_path)
|
|
16313
|
-
content.file_path = mcp.file_path;
|
|
16314
|
-
break;
|
|
16315
|
-
}
|
|
16316
|
-
}
|
|
16317
|
-
if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
|
|
16318
|
-
const firstUrl = signals.url_checks[0]?.url;
|
|
16319
|
-
if (firstUrl)
|
|
16320
|
-
content.url = firstUrl;
|
|
16321
|
-
}
|
|
16322
|
-
if (signals.package_checks && signals.package_checks.length > 0) {
|
|
16323
|
-
const p = signals.package_checks[0];
|
|
16324
|
-
if (p?.package_name)
|
|
16325
|
-
content.package_name = p.package_name;
|
|
16326
|
-
if (p?.package_version)
|
|
16327
|
-
content.package_version = p.package_version;
|
|
16328
|
-
if (p?.package_registry)
|
|
16329
|
-
content.package_registry = p.package_registry;
|
|
16330
|
-
}
|
|
16331
|
-
applyFieldLimits(content);
|
|
16332
|
-
return content;
|
|
16560
|
+
function amsiReportingData(amsiResult) {
|
|
16561
|
+
const ruleName = AMSI_REPORTING_RULE_NAMES[amsiDetectionClass(amsiResult)];
|
|
16562
|
+
return `${ruleName}:${Math.trunc(amsiResult).toString(16)}`;
|
|
16333
16563
|
}
|
|
16334
16564
|
|
|
16335
16565
|
// ../core/dist/extended-info.js
|
|
16336
|
-
var
|
|
16566
|
+
var import_promises4 = require("node:fs/promises");
|
|
16337
16567
|
var import_node_os3 = require("node:os");
|
|
16338
|
-
var
|
|
16568
|
+
var import_node_path11 = require("node:path");
|
|
16339
16569
|
init_file_utils();
|
|
16340
16570
|
init_types2();
|
|
16341
16571
|
var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
|
|
@@ -16401,8 +16631,8 @@ function sanitizeDocument(parsed, logger) {
|
|
|
16401
16631
|
return out;
|
|
16402
16632
|
}
|
|
16403
16633
|
async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
16404
|
-
const sageDir = sageDirPath ?? (0,
|
|
16405
|
-
const filePath = (0,
|
|
16634
|
+
const sageDir = sageDirPath ?? (0, import_node_path11.join)((0, import_node_os3.homedir)(), ".sage");
|
|
16635
|
+
const filePath = (0, import_node_path11.join)(sageDir, EXTENDED_INFO_FILENAME);
|
|
16406
16636
|
const cached = cache.get(filePath);
|
|
16407
16637
|
if (cached)
|
|
16408
16638
|
return cached.value;
|
|
@@ -16413,7 +16643,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
|
16413
16643
|
async function loadExtendedInfoUncached(filePath, logger) {
|
|
16414
16644
|
let size;
|
|
16415
16645
|
try {
|
|
16416
|
-
const info = await (0,
|
|
16646
|
+
const info = await (0, import_promises4.stat)(filePath);
|
|
16417
16647
|
if (!info.isFile()) {
|
|
16418
16648
|
logger.debug(`extended-info: not a regular file at ${filePath}`);
|
|
16419
16649
|
return null;
|
|
@@ -16501,13 +16731,13 @@ function mergeExtendedInfo(envelope, extendedInfo) {
|
|
|
16501
16731
|
|
|
16502
16732
|
// ../core/dist/installation-id.js
|
|
16503
16733
|
var import_node_crypto4 = require("node:crypto");
|
|
16504
|
-
var
|
|
16505
|
-
var
|
|
16734
|
+
var import_promises5 = require("node:fs/promises");
|
|
16735
|
+
var import_node_path12 = require("node:path");
|
|
16506
16736
|
init_config();
|
|
16507
16737
|
init_file_utils();
|
|
16508
16738
|
async function getInstallationId(sageDirPath) {
|
|
16509
16739
|
const sageDir = sageDirPath ?? resolvePath("~/.sage");
|
|
16510
|
-
const idPath = (0,
|
|
16740
|
+
const idPath = (0, import_node_path12.join)(sageDir, "installation-id");
|
|
16511
16741
|
let fileExists = false;
|
|
16512
16742
|
try {
|
|
16513
16743
|
const existing = await getFileContent(idPath, "utf-8");
|
|
@@ -16519,8 +16749,8 @@ async function getInstallationId(sageDirPath) {
|
|
|
16519
16749
|
}
|
|
16520
16750
|
try {
|
|
16521
16751
|
const id = (0, import_node_crypto4.randomUUID)();
|
|
16522
|
-
await (0,
|
|
16523
|
-
await (0,
|
|
16752
|
+
await (0, import_promises5.mkdir)(sageDir, { recursive: true, mode: 448 });
|
|
16753
|
+
await (0, import_promises5.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
|
|
16524
16754
|
return id;
|
|
16525
16755
|
} catch (err) {
|
|
16526
16756
|
if (err.code === "EEXIST") {
|
|
@@ -16549,10 +16779,10 @@ function resolveTimeoutMs() {
|
|
|
16549
16779
|
const ms = Math.floor(parsed * 1e3);
|
|
16550
16780
|
return Math.min(ms, MAX_EFFECTIVE_TIMEOUT_MS);
|
|
16551
16781
|
}
|
|
16552
|
-
async function
|
|
16782
|
+
async function sendCommunityIqTelemetry(args) {
|
|
16553
16783
|
const logger = args.logger ?? nullLogger;
|
|
16554
16784
|
if (!args.communityIqEnabled) {
|
|
16555
|
-
logger.debug("Community IQ disabled, skipping
|
|
16785
|
+
logger.debug("Community IQ disabled, skipping telemetry");
|
|
16556
16786
|
return;
|
|
16557
16787
|
}
|
|
16558
16788
|
let iid;
|
|
@@ -16561,15 +16791,17 @@ async function sendCommunityIqDetection(args) {
|
|
|
16561
16791
|
} catch {
|
|
16562
16792
|
}
|
|
16563
16793
|
if (!iid) {
|
|
16564
|
-
logger.debug("Skipping
|
|
16794
|
+
logger.debug("Skipping telemetry: missing installation id");
|
|
16565
16795
|
return;
|
|
16566
16796
|
}
|
|
16567
16797
|
const envelope = buildSageProxyEnvelope({
|
|
16568
16798
|
iid,
|
|
16569
16799
|
versionApp: VERSION,
|
|
16570
16800
|
agentRuntime: args.agentRuntime ?? "unknown",
|
|
16571
|
-
agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown"
|
|
16801
|
+
agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown",
|
|
16802
|
+
config: args.config
|
|
16572
16803
|
});
|
|
16804
|
+
const blocking = args.blocking ?? true;
|
|
16573
16805
|
const payload = {
|
|
16574
16806
|
...envelope,
|
|
16575
16807
|
block_event: {
|
|
@@ -16578,11 +16810,12 @@ async function sendCommunityIqDetection(args) {
|
|
|
16578
16810
|
// canonicalize before calling `evaluateToolCall`, so no further
|
|
16579
16811
|
// mapping is needed here.
|
|
16580
16812
|
tool_type: args.toolName,
|
|
16581
|
-
verdict: "deny",
|
|
16582
|
-
user_action: "blocked",
|
|
16813
|
+
verdict: blocking ? "deny" : "suspicious",
|
|
16814
|
+
...blocking ? { user_action: "blocked" } : {},
|
|
16583
16815
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16584
16816
|
...args.signals && Object.keys(args.signals).length > 0 ? { signals: args.signals } : {},
|
|
16585
|
-
content: args.content ?? {}
|
|
16817
|
+
content: args.content ?? {},
|
|
16818
|
+
...args.contentSnippet ? { content_snippet: args.contentSnippet } : {}
|
|
16586
16819
|
},
|
|
16587
16820
|
event_id: args.eventId,
|
|
16588
16821
|
comment: ""
|
|
@@ -16590,20 +16823,21 @@ async function sendCommunityIqDetection(args) {
|
|
|
16590
16823
|
const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
|
|
16591
16824
|
const enriched = mergeExtendedInfo(payload, extendedInfo);
|
|
16592
16825
|
const timeoutMs = resolveTimeoutMs();
|
|
16826
|
+
const endpoint = blocking ? "/v2/detection" : "/v2/heuristic";
|
|
16593
16827
|
try {
|
|
16594
|
-
const response = await fetch(resolveEndpoint(
|
|
16828
|
+
const response = await fetch(resolveEndpoint(endpoint), {
|
|
16595
16829
|
method: "POST",
|
|
16596
16830
|
headers: { "Content-Type": "application/json" },
|
|
16597
16831
|
body: JSON.stringify(enriched),
|
|
16598
16832
|
signal: AbortSignal.timeout(timeoutMs)
|
|
16599
16833
|
});
|
|
16600
16834
|
if (!response.ok) {
|
|
16601
|
-
logger.warn("
|
|
16835
|
+
logger.warn("Community IQ telemetry send failed", {
|
|
16602
16836
|
eventId: args.eventId,
|
|
16603
16837
|
status: response.status
|
|
16604
16838
|
});
|
|
16605
16839
|
} else {
|
|
16606
|
-
logger.debug("
|
|
16840
|
+
logger.debug("Community IQ telemetry sent", {
|
|
16607
16841
|
eventId: args.eventId,
|
|
16608
16842
|
toolName: args.toolName,
|
|
16609
16843
|
hookType: args.hookType ?? "PreToolUse"
|
|
@@ -16617,6 +16851,29 @@ async function sendCommunityIqDetection(args) {
|
|
|
16617
16851
|
}
|
|
16618
16852
|
}
|
|
16619
16853
|
|
|
16854
|
+
// ../core/dist/e2e-capture.js
|
|
16855
|
+
var import_node_path13 = require("node:path");
|
|
16856
|
+
var CAPTURE_MAX_BYTES = 10 * 1024 * 1024;
|
|
16857
|
+
var CAPTURE_MAX_FILES = 2;
|
|
16858
|
+
function captureEnabled() {
|
|
16859
|
+
return !!process.env.SAGE_E2E_CAPTURE_DIR;
|
|
16860
|
+
}
|
|
16861
|
+
async function captureHookInput(event, raw, normalized, options = {}) {
|
|
16862
|
+
const dir = process.env.SAGE_E2E_CAPTURE_DIR;
|
|
16863
|
+
if (!dir)
|
|
16864
|
+
return;
|
|
16865
|
+
try {
|
|
16866
|
+
const prefix = options.filePrefix ?? "";
|
|
16867
|
+
const base = event === "PreToolUse" ? "pre-tool-use.jsonl" : "post-tool-use.jsonl";
|
|
16868
|
+
await appendJsonlEntry({
|
|
16869
|
+
path: (0, import_node_path13.join)(dir, `${prefix}${base}`),
|
|
16870
|
+
max_bytes: CAPTURE_MAX_BYTES,
|
|
16871
|
+
max_files: CAPTURE_MAX_FILES
|
|
16872
|
+
}, { event, raw, normalized });
|
|
16873
|
+
} catch {
|
|
16874
|
+
}
|
|
16875
|
+
}
|
|
16876
|
+
|
|
16620
16877
|
// ../core/dist/policy.js
|
|
16621
16878
|
init_types2();
|
|
16622
16879
|
var SENSITIVITY_POLICY = {
|
|
@@ -16840,27 +17097,27 @@ init_config();
|
|
|
16840
17097
|
|
|
16841
17098
|
// ../core/dist/exceptions.js
|
|
16842
17099
|
var import_node_crypto5 = require("node:crypto");
|
|
16843
|
-
var
|
|
17100
|
+
var import_node_path15 = require("node:path");
|
|
16844
17101
|
init_config();
|
|
16845
17102
|
init_file_utils();
|
|
16846
17103
|
|
|
16847
17104
|
// ../core/dist/trusted-domains.js
|
|
16848
|
-
var
|
|
16849
|
-
var
|
|
17105
|
+
var import_promises6 = require("node:fs/promises");
|
|
17106
|
+
var import_node_path14 = require("node:path");
|
|
16850
17107
|
var import_yaml = __toESM(require_dist(), 1);
|
|
16851
17108
|
init_file_utils();
|
|
16852
17109
|
init_types2();
|
|
16853
17110
|
async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
|
|
16854
17111
|
let files;
|
|
16855
17112
|
try {
|
|
16856
|
-
files = (await (0,
|
|
17113
|
+
files = (await (0, import_promises6.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
16857
17114
|
} catch {
|
|
16858
17115
|
logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
|
|
16859
17116
|
return [];
|
|
16860
17117
|
}
|
|
16861
17118
|
const domains = [];
|
|
16862
17119
|
for (const filename of files) {
|
|
16863
|
-
const filePath = (0,
|
|
17120
|
+
const filePath = (0, import_node_path14.join)(trustedDomainsDir, filename);
|
|
16864
17121
|
let content;
|
|
16865
17122
|
try {
|
|
16866
17123
|
content = await getFileContent(filePath);
|
|
@@ -17136,7 +17393,7 @@ function matchesDomain(pattern, url) {
|
|
|
17136
17393
|
function normalizePatternPath(pattern) {
|
|
17137
17394
|
const home = getHomeDir();
|
|
17138
17395
|
const expanded = pattern.startsWith("~/") || pattern === "~" ? `${home}${pattern.slice(1)}` : pattern;
|
|
17139
|
-
return (0,
|
|
17396
|
+
return (0, import_node_path15.normalize)(expanded);
|
|
17140
17397
|
}
|
|
17141
17398
|
function matchesPath(pattern, filePath) {
|
|
17142
17399
|
const hasWildcard = pattern.includes("*");
|
|
@@ -17147,7 +17404,7 @@ function matchesPath(pattern, filePath) {
|
|
|
17147
17404
|
const normalizedPath = normalizePatternPath(filePath);
|
|
17148
17405
|
if (normalizedPath === normalizedPattern)
|
|
17149
17406
|
return true;
|
|
17150
|
-
if (normalizedPath.startsWith(normalizedPattern +
|
|
17407
|
+
if (normalizedPath.startsWith(normalizedPattern + import_node_path15.sep))
|
|
17151
17408
|
return true;
|
|
17152
17409
|
return false;
|
|
17153
17410
|
}
|
|
@@ -17463,6 +17720,7 @@ var PackageChecker = class {
|
|
|
17463
17720
|
if (pkg.name.startsWith("@")) {
|
|
17464
17721
|
results.push({
|
|
17465
17722
|
packageName: pkg.name,
|
|
17723
|
+
packageVersion: pkg.version,
|
|
17466
17724
|
registry: pkg.registry,
|
|
17467
17725
|
verdict: "clean",
|
|
17468
17726
|
confidence: 1,
|
|
@@ -17483,6 +17741,7 @@ var PackageChecker = class {
|
|
|
17483
17741
|
} catch {
|
|
17484
17742
|
return {
|
|
17485
17743
|
packageName: pkg.name,
|
|
17744
|
+
packageVersion: pkg.version,
|
|
17486
17745
|
registry: pkg.registry,
|
|
17487
17746
|
verdict: "unknown",
|
|
17488
17747
|
confidence: 0.6,
|
|
@@ -17492,6 +17751,7 @@ var PackageChecker = class {
|
|
|
17492
17751
|
if (metadata === null) {
|
|
17493
17752
|
return {
|
|
17494
17753
|
packageName: pkg.name,
|
|
17754
|
+
packageVersion: pkg.version,
|
|
17495
17755
|
registry: pkg.registry,
|
|
17496
17756
|
verdict: "not_found",
|
|
17497
17757
|
confidence: 0.95,
|
|
@@ -17501,6 +17761,7 @@ var PackageChecker = class {
|
|
|
17501
17761
|
if (!metadata.requestedVersionFound && pkg.version) {
|
|
17502
17762
|
return {
|
|
17503
17763
|
packageName: pkg.name,
|
|
17764
|
+
packageVersion: pkg.version,
|
|
17504
17765
|
registry: pkg.registry,
|
|
17505
17766
|
verdict: "not_found",
|
|
17506
17767
|
confidence: 0.95,
|
|
@@ -17516,6 +17777,7 @@ var PackageChecker = class {
|
|
|
17516
17777
|
const detections = fileResult.detectionNames.length > 0 ? fileResult.detectionNames.join(", ") : sev;
|
|
17517
17778
|
return {
|
|
17518
17779
|
packageName: pkg.name,
|
|
17780
|
+
packageVersion: pkg.version,
|
|
17519
17781
|
registry: pkg.registry,
|
|
17520
17782
|
verdict: "malicious",
|
|
17521
17783
|
confidence: 1,
|
|
@@ -17534,6 +17796,7 @@ var PackageChecker = class {
|
|
|
17534
17796
|
if (ageDays !== void 0 && ageDays < SUSPICIOUS_AGE_DAYS) {
|
|
17535
17797
|
return {
|
|
17536
17798
|
packageName: pkg.name,
|
|
17799
|
+
packageVersion: pkg.version,
|
|
17537
17800
|
registry: pkg.registry,
|
|
17538
17801
|
verdict: "suspicious_age",
|
|
17539
17802
|
confidence: 0.6,
|
|
@@ -17543,6 +17806,7 @@ var PackageChecker = class {
|
|
|
17543
17806
|
}
|
|
17544
17807
|
return {
|
|
17545
17808
|
packageName: pkg.name,
|
|
17809
|
+
packageVersion: pkg.version,
|
|
17546
17810
|
registry: pkg.registry,
|
|
17547
17811
|
verdict: "clean",
|
|
17548
17812
|
confidence: 1,
|
|
@@ -17927,7 +18191,7 @@ function extractFromRequirementsTxt(content) {
|
|
|
17927
18191
|
}
|
|
17928
18192
|
|
|
17929
18193
|
// ../core/dist/statusline.js
|
|
17930
|
-
var
|
|
18194
|
+
var import_node_path16 = require("node:path");
|
|
17931
18195
|
init_config();
|
|
17932
18196
|
init_file_utils();
|
|
17933
18197
|
var STATUS_PREFIX = "statusline-";
|
|
@@ -17936,15 +18200,17 @@ function sanitizeSessionId(sessionId) {
|
|
|
17936
18200
|
return sessionId.replace(/[^a-zA-Z0-9-]/g, "_");
|
|
17937
18201
|
}
|
|
17938
18202
|
function statusFilePath(sessionId) {
|
|
17939
|
-
return (0,
|
|
18203
|
+
return (0, import_node_path16.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
|
|
17940
18204
|
}
|
|
17941
18205
|
function emptyStatus() {
|
|
18206
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17942
18207
|
return {
|
|
17943
18208
|
denied: 0,
|
|
17944
18209
|
flagged: 0,
|
|
17945
18210
|
lastCategory: null,
|
|
17946
18211
|
lastReason: null,
|
|
17947
|
-
updatedAt:
|
|
18212
|
+
updatedAt: now,
|
|
18213
|
+
startedAt: now
|
|
17948
18214
|
};
|
|
17949
18215
|
}
|
|
17950
18216
|
async function readStatus(sessionId) {
|
|
@@ -17969,13 +18235,15 @@ async function updateSessionStatus(sessionId, verdict) {
|
|
|
17969
18235
|
}
|
|
17970
18236
|
|
|
17971
18237
|
// ../core/dist/threat-loader.js
|
|
17972
|
-
var
|
|
17973
|
-
var
|
|
18238
|
+
var import_promises7 = require("node:fs/promises");
|
|
18239
|
+
var import_node_path17 = require("node:path");
|
|
17974
18240
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
17975
18241
|
init_file_utils();
|
|
17976
18242
|
init_types2();
|
|
17977
18243
|
var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
|
|
17978
18244
|
"id",
|
|
18245
|
+
"version",
|
|
18246
|
+
"detection_name",
|
|
17979
18247
|
"category",
|
|
17980
18248
|
"severity",
|
|
17981
18249
|
"confidence",
|
|
@@ -17983,6 +18251,37 @@ var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
|
|
|
17983
18251
|
"match_on",
|
|
17984
18252
|
"title"
|
|
17985
18253
|
]);
|
|
18254
|
+
var MACRO_FILENAME = "_macros.yaml";
|
|
18255
|
+
var MAX_MACRO_PASSES = 5;
|
|
18256
|
+
var MACRO_REF = /\{\{([A-Z0-9_]+)\}\}/g;
|
|
18257
|
+
function expandMacros(pattern, macros) {
|
|
18258
|
+
let expanded = pattern;
|
|
18259
|
+
for (let pass = 0; pass <= MAX_MACRO_PASSES; pass++) {
|
|
18260
|
+
if (!expanded.includes("{{"))
|
|
18261
|
+
return expanded;
|
|
18262
|
+
if (pass === MAX_MACRO_PASSES) {
|
|
18263
|
+
throw new Error(`macro nesting exceeds ${MAX_MACRO_PASSES} passes`);
|
|
18264
|
+
}
|
|
18265
|
+
expanded = expanded.replace(MACRO_REF, (_match, name) => {
|
|
18266
|
+
const value = macros[name];
|
|
18267
|
+
if (value === void 0)
|
|
18268
|
+
throw new Error(`undefined macro {{${name}}}`);
|
|
18269
|
+
return value;
|
|
18270
|
+
});
|
|
18271
|
+
}
|
|
18272
|
+
return expanded;
|
|
18273
|
+
}
|
|
18274
|
+
function readMacros(filename, data, logger) {
|
|
18275
|
+
const macros = {};
|
|
18276
|
+
for (const [name, value] of Object.entries(data)) {
|
|
18277
|
+
if (typeof value !== "string") {
|
|
18278
|
+
logger.warn(`Skipping macro ${name} in ${filename}: not a string`);
|
|
18279
|
+
continue;
|
|
18280
|
+
}
|
|
18281
|
+
macros[name] = value;
|
|
18282
|
+
}
|
|
18283
|
+
return macros;
|
|
18284
|
+
}
|
|
17986
18285
|
function parseExpiresAt(value) {
|
|
17987
18286
|
if (value == null)
|
|
17988
18287
|
return null;
|
|
@@ -18003,16 +18302,17 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18003
18302
|
const threats = [];
|
|
18004
18303
|
let files;
|
|
18005
18304
|
try {
|
|
18006
|
-
files = (await (0,
|
|
18305
|
+
files = (await (0, import_promises7.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
18007
18306
|
} catch {
|
|
18008
18307
|
logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
|
|
18009
18308
|
return threats;
|
|
18010
18309
|
}
|
|
18310
|
+
const ruleFiles = [];
|
|
18311
|
+
let macros = {};
|
|
18011
18312
|
for (const filename of files) {
|
|
18012
|
-
const filePath = (0, import_node_path15.join)(threatDir, filename);
|
|
18013
18313
|
let content;
|
|
18014
18314
|
try {
|
|
18015
|
-
content = await getFileContent(
|
|
18315
|
+
content = await getFileContent((0, import_node_path17.join)(threatDir, filename));
|
|
18016
18316
|
} catch (e) {
|
|
18017
18317
|
logger.warn(`Failed to read ${filename}`, { error: String(e) });
|
|
18018
18318
|
continue;
|
|
@@ -18024,10 +18324,21 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18024
18324
|
logger.warn(`Failed to parse ${filename}`, { error: String(e) });
|
|
18025
18325
|
continue;
|
|
18026
18326
|
}
|
|
18327
|
+
if (filename === MACRO_FILENAME) {
|
|
18328
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
18329
|
+
logger.warn(`Expected mapping in ${filename}, got ${Array.isArray(data) ? "list" : typeof data}`);
|
|
18330
|
+
continue;
|
|
18331
|
+
}
|
|
18332
|
+
macros = readMacros(filename, data, logger);
|
|
18333
|
+
continue;
|
|
18334
|
+
}
|
|
18027
18335
|
if (!Array.isArray(data)) {
|
|
18028
18336
|
logger.warn(`Expected list in ${filename}, got ${typeof data}`);
|
|
18029
18337
|
continue;
|
|
18030
18338
|
}
|
|
18339
|
+
ruleFiles.push({ filename, data });
|
|
18340
|
+
}
|
|
18341
|
+
for (const { filename, data } of ruleFiles) {
|
|
18031
18342
|
for (const entry of data) {
|
|
18032
18343
|
if (typeof entry !== "object" || entry === null) {
|
|
18033
18344
|
logger.warn(`Skipping non-object entry in ${filename}`);
|
|
@@ -18044,10 +18355,12 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18044
18355
|
continue;
|
|
18045
18356
|
if (isExpired(record))
|
|
18046
18357
|
continue;
|
|
18358
|
+
let pattern;
|
|
18047
18359
|
let compiledPattern;
|
|
18048
18360
|
try {
|
|
18361
|
+
pattern = expandMacros(record.pattern, macros);
|
|
18049
18362
|
const flags2 = record.case_insensitive === true ? "i" : "";
|
|
18050
|
-
compiledPattern = new RegExp(
|
|
18363
|
+
compiledPattern = new RegExp(pattern, flags2);
|
|
18051
18364
|
} catch (e) {
|
|
18052
18365
|
logger.warn(`Skipping threat ${record.id}: invalid regex pattern`, {
|
|
18053
18366
|
error: String(e)
|
|
@@ -18065,13 +18378,28 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18065
18378
|
});
|
|
18066
18379
|
continue;
|
|
18067
18380
|
}
|
|
18381
|
+
const version = record.version;
|
|
18382
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version <= 0) {
|
|
18383
|
+
logger.warn(`Skipping threat ${record.id}: invalid version`, {
|
|
18384
|
+
version
|
|
18385
|
+
});
|
|
18386
|
+
continue;
|
|
18387
|
+
}
|
|
18388
|
+
const detectionName = record.detection_name;
|
|
18389
|
+
if (typeof detectionName !== "string" || !isCanonicalDetectionName(detectionName)) {
|
|
18390
|
+
logger.warn(`Skipping threat ${record.id}: invalid detection_name`, {
|
|
18391
|
+
detectionName
|
|
18392
|
+
});
|
|
18393
|
+
continue;
|
|
18394
|
+
}
|
|
18068
18395
|
threats.push({
|
|
18069
18396
|
id: record.id,
|
|
18070
|
-
version
|
|
18397
|
+
version,
|
|
18398
|
+
detectionName,
|
|
18071
18399
|
category: record.category,
|
|
18072
18400
|
severity: record.severity,
|
|
18073
18401
|
confidence,
|
|
18074
|
-
pattern
|
|
18402
|
+
pattern,
|
|
18075
18403
|
compiledPattern,
|
|
18076
18404
|
matchOn,
|
|
18077
18405
|
title: record.title,
|
|
@@ -18089,6 +18417,29 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18089
18417
|
init_types2();
|
|
18090
18418
|
var AMSI_CONTENT_SNIPPET_MAX = 200;
|
|
18091
18419
|
var AMSI_CONTENT_NAME_MAX = 256;
|
|
18420
|
+
var HEURISTIC_PI_CONTENT_SNIPPET_MAX = 200;
|
|
18421
|
+
function buildHeuristicSignal(match) {
|
|
18422
|
+
return {
|
|
18423
|
+
detection_name: formatReportingDetectionName(match.threat.detectionName, "heuristics", `${match.threat.id}:${match.threat.version}`),
|
|
18424
|
+
rule_id: match.threat.id,
|
|
18425
|
+
rule_version: match.threat.version
|
|
18426
|
+
};
|
|
18427
|
+
}
|
|
18428
|
+
function resolvePiContentSnippet(piResults, heuristicMatches) {
|
|
18429
|
+
let highestRiskResult;
|
|
18430
|
+
for (const result of piResults) {
|
|
18431
|
+
if (result.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && result.contentSnippet && (!highestRiskResult || result.risk > highestRiskResult.risk)) {
|
|
18432
|
+
highestRiskResult = result;
|
|
18433
|
+
}
|
|
18434
|
+
}
|
|
18435
|
+
if (highestRiskResult?.contentSnippet)
|
|
18436
|
+
return scrubHomePath(highestRiskResult.contentSnippet);
|
|
18437
|
+
const heuristicMatch = heuristicMatches.find((match) => match.threat.category === "prompt_injection");
|
|
18438
|
+
if (!heuristicMatch)
|
|
18439
|
+
return void 0;
|
|
18440
|
+
const snippet = safeTruncate(scrubHomePath(heuristicMatch.matchValue), HEURISTIC_PI_CONTENT_SNIPPET_MAX);
|
|
18441
|
+
return snippet || void 0;
|
|
18442
|
+
}
|
|
18092
18443
|
function scrubAmsiContentName(contentName) {
|
|
18093
18444
|
const colon = contentName.indexOf(":");
|
|
18094
18445
|
if (colon < 0)
|
|
@@ -18098,16 +18449,10 @@ function scrubAmsiContentName(contentName) {
|
|
|
18098
18449
|
return `${head}${scrubHomePath(tail)}`;
|
|
18099
18450
|
}
|
|
18100
18451
|
function buildAmsiSignal(r) {
|
|
18101
|
-
const detectionName = r.amsiResult >= 32768 ? "AMSI|DETECTED" : r.amsiResult >= 16384 ? "AMSI|BLOCKED_BY_ADMIN" : (
|
|
18102
|
-
// Defensive: callers should filter these out via `isDetected || isBlockedByAdmin`,
|
|
18103
|
-
// but if a non-detected/non-blocked result still reaches here we emit a
|
|
18104
|
-
// meaningful label rather than silently dropping the entry.
|
|
18105
|
-
"AMSI|UNKNOWN"
|
|
18106
|
-
);
|
|
18107
18452
|
const contentName = safeTruncate(scrubAmsiContentName(r.contentName), AMSI_CONTENT_NAME_MAX);
|
|
18108
18453
|
const snippet = r.content ? safeTruncate(scrubHomePath(r.content), AMSI_CONTENT_SNIPPET_MAX) : "";
|
|
18109
18454
|
return {
|
|
18110
|
-
detection_name:
|
|
18455
|
+
detection_name: formatReportingDetectionName(amsiDetectionName(r.amsiResult), "amsi", amsiReportingData(r.amsiResult)),
|
|
18111
18456
|
content_name: contentName,
|
|
18112
18457
|
amsi_result: r.amsiResult,
|
|
18113
18458
|
...snippet ? { content_snippet: snippet } : {}
|
|
@@ -18179,7 +18524,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18179
18524
|
severity: "critical",
|
|
18180
18525
|
source: "exception",
|
|
18181
18526
|
artifacts: [denyMatch.artifact.value],
|
|
18182
|
-
matchedThreatId:
|
|
18527
|
+
matchedThreatId: denyMatch.rule.id,
|
|
18183
18528
|
reasons: [
|
|
18184
18529
|
`Deny exception: ${denyMatch.rule.match} pattern '${denyMatch.rule.pattern}'${denyMatch.rule.reason ? ` \u2014 ${denyMatch.rule.reason}` : ""}`
|
|
18185
18530
|
]
|
|
@@ -18349,6 +18694,12 @@ async function evaluateToolCall(request, context) {
|
|
|
18349
18694
|
if (result) {
|
|
18350
18695
|
allPiResults.push(result);
|
|
18351
18696
|
}
|
|
18697
|
+
} else {
|
|
18698
|
+
logger.info("PI model inference skipped", {
|
|
18699
|
+
context: `WebFetch:${url}`,
|
|
18700
|
+
reason: "content_not_scannable",
|
|
18701
|
+
contentType: fetched.contentType ?? "unknown"
|
|
18702
|
+
});
|
|
18352
18703
|
}
|
|
18353
18704
|
}
|
|
18354
18705
|
}
|
|
@@ -18365,8 +18716,8 @@ async function evaluateToolCall(request, context) {
|
|
|
18365
18716
|
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
|
|
18366
18717
|
piCheckResults: allPiResults,
|
|
18367
18718
|
piThresholds: {
|
|
18368
|
-
highRisk:
|
|
18369
|
-
mediumRisk:
|
|
18719
|
+
highRisk: DEFAULT_PI_HIGH_RISK_THRESHOLD,
|
|
18720
|
+
mediumRisk: DEFAULT_PI_MEDIUM_RISK_THRESHOLD
|
|
18370
18721
|
}
|
|
18371
18722
|
});
|
|
18372
18723
|
} else {
|
|
@@ -18394,24 +18745,9 @@ async function evaluateToolCall(request, context) {
|
|
|
18394
18745
|
}
|
|
18395
18746
|
}
|
|
18396
18747
|
await cacheUrlResults(urlCheckResults, cache2);
|
|
18397
|
-
function formatPackageDetectionName(p) {
|
|
18398
|
-
const base = `PKG|${p.verdict}|registry=${p.registry}|name=${p.packageName}`;
|
|
18399
|
-
if (p.verdict === "suspicious_age") {
|
|
18400
|
-
const ageDays = typeof p.ageDays === "number" ? Math.floor(p.ageDays) : void 0;
|
|
18401
|
-
return ageDays !== void 0 ? `${base}|age_days=${ageDays}` : base;
|
|
18402
|
-
}
|
|
18403
|
-
if (p.verdict === "malicious") {
|
|
18404
|
-
const det = (p.fileDetectionNames ?? []).filter((d) => typeof d === "string" && d.length > 0);
|
|
18405
|
-
return det.length > 0 ? `${base}|det=${det.join(",")}` : base;
|
|
18406
|
-
}
|
|
18407
|
-
return base;
|
|
18408
|
-
}
|
|
18409
18748
|
const auditSignals = {};
|
|
18410
18749
|
if (heuristicMatches.length > 0) {
|
|
18411
|
-
auditSignals.heuristics = heuristicMatches.map(
|
|
18412
|
-
rule_id: m.threat.id,
|
|
18413
|
-
rule_version: typeof m.threat.version === "number" ? m.threat.version : void 0
|
|
18414
|
-
}));
|
|
18750
|
+
auditSignals.heuristics = heuristicMatches.map(buildHeuristicSignal);
|
|
18415
18751
|
}
|
|
18416
18752
|
if (urlCheckResults.length > 0) {
|
|
18417
18753
|
const relevant = urlCheckResults.filter((r) => r.isMalicious);
|
|
@@ -18442,9 +18778,9 @@ async function evaluateToolCall(request, context) {
|
|
|
18442
18778
|
const relevant = packageCheckResults.filter((p) => p.verdict !== "clean");
|
|
18443
18779
|
if (relevant.length > 0) {
|
|
18444
18780
|
auditSignals.package_checks = relevant.map((p) => ({
|
|
18445
|
-
detection_name:
|
|
18781
|
+
detection_name: formatReportingDetectionName(packageDetectionName(p.verdict), "package", p.packageVersion ? `${p.packageName}:${p.packageVersion}` : p.packageName),
|
|
18446
18782
|
package_name: p.packageName,
|
|
18447
|
-
package_version:
|
|
18783
|
+
package_version: p.packageVersion,
|
|
18448
18784
|
package_registry: p.registry
|
|
18449
18785
|
}));
|
|
18450
18786
|
}
|
|
@@ -18457,8 +18793,9 @@ async function evaluateToolCall(request, context) {
|
|
|
18457
18793
|
}
|
|
18458
18794
|
}
|
|
18459
18795
|
if (allPiResults.length > 0) {
|
|
18460
|
-
const piSnippetFloor =
|
|
18796
|
+
const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
|
|
18461
18797
|
auditSignals.pi_checks = allPiResults.map((r) => ({
|
|
18798
|
+
detection_name: formatReportingDetectionName(PI_DETECTION_NAME, "ml", `${r.modelId}:${MODEL_SCHEMA_VERSION}`),
|
|
18462
18799
|
risk: r.risk,
|
|
18463
18800
|
model_id: r.modelId,
|
|
18464
18801
|
content_name: r.contentName,
|
|
@@ -18472,6 +18809,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18472
18809
|
}
|
|
18473
18810
|
}
|
|
18474
18811
|
const resolvedSignals = Object.keys(auditSignals).length > 0 ? auditSignals : void 0;
|
|
18812
|
+
const contentSnippet = resolvePiContentSnippet(allPiResults, heuristicMatches);
|
|
18475
18813
|
const builtContent = buildContentSnapshot(request.toolName, request.toolInput, request.artifacts, auditSignals);
|
|
18476
18814
|
const resolvedContent = Object.keys(builtContent).length > 0 ? builtContent : void 0;
|
|
18477
18815
|
try {
|
|
@@ -18485,28 +18823,37 @@ async function evaluateToolCall(request, context) {
|
|
|
18485
18823
|
hookType: request.hookType,
|
|
18486
18824
|
signals: resolvedSignals,
|
|
18487
18825
|
content: resolvedContent,
|
|
18826
|
+
contentSnippet,
|
|
18488
18827
|
eventId,
|
|
18489
18828
|
toolUseId: request.toolUseId
|
|
18490
18829
|
});
|
|
18491
18830
|
} catch (error) {
|
|
18492
18831
|
logger.debug("Audit verdict logging failed open", { error: String(error) });
|
|
18493
18832
|
}
|
|
18494
|
-
|
|
18833
|
+
const submitTelemetry = async (blocking) => {
|
|
18495
18834
|
try {
|
|
18496
|
-
await
|
|
18835
|
+
await sendCommunityIqTelemetry({
|
|
18497
18836
|
eventId,
|
|
18498
18837
|
agentRuntime: request.agentRuntime,
|
|
18499
18838
|
agentRuntimeVersion: request.agentRuntimeVersion,
|
|
18500
18839
|
hookType: request.hookType,
|
|
18501
18840
|
toolName: request.toolName,
|
|
18502
18841
|
content: resolvedContent,
|
|
18842
|
+
contentSnippet,
|
|
18503
18843
|
signals: resolvedSignals,
|
|
18504
18844
|
communityIqEnabled: config2.community_iq,
|
|
18845
|
+
config: config2,
|
|
18846
|
+
blocking,
|
|
18505
18847
|
logger
|
|
18506
18848
|
});
|
|
18507
18849
|
} catch (error) {
|
|
18508
|
-
logger.debug("
|
|
18850
|
+
logger.debug("Community IQ telemetry failed open", { error: String(error) });
|
|
18509
18851
|
}
|
|
18852
|
+
};
|
|
18853
|
+
if (verdict.decision === "deny") {
|
|
18854
|
+
await submitTelemetry(true);
|
|
18855
|
+
} else if (allPiResults.some((r) => r.risk >= DEFAULT_PI_TELEMETRY_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD)) {
|
|
18856
|
+
await submitTelemetry(false);
|
|
18510
18857
|
}
|
|
18511
18858
|
if (verdict.decision !== "allow") {
|
|
18512
18859
|
try {
|
|
@@ -18515,7 +18862,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18515
18862
|
logger.debug("Session status update failed open", { error: String(error) });
|
|
18516
18863
|
}
|
|
18517
18864
|
}
|
|
18518
|
-
const piWarnings = allPiResults.filter((r) => r.risk >=
|
|
18865
|
+
const piWarnings = allPiResults.filter((r) => r.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD);
|
|
18519
18866
|
if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
|
|
18520
18867
|
verdict.piWarnings = piWarnings;
|
|
18521
18868
|
logEvaluationCompleted(verdict);
|
|
@@ -18618,6 +18965,7 @@ async function checkPackages(request, config2, cache2, logger) {
|
|
|
18618
18965
|
}
|
|
18619
18966
|
results.push({
|
|
18620
18967
|
packageName: pkg.name,
|
|
18968
|
+
packageVersion: pkg.version,
|
|
18621
18969
|
registry: pkg.registry,
|
|
18622
18970
|
verdict: rawVerdict,
|
|
18623
18971
|
confidence: rawConf,
|
|
@@ -18657,7 +19005,7 @@ function createPiProvider(config2, _context, logger) {
|
|
|
18657
19005
|
return new BundledPiProvider({
|
|
18658
19006
|
modelPath: config2.pi_check.model_path,
|
|
18659
19007
|
maxContentLength: config2.pi_check.max_content_length,
|
|
18660
|
-
mediumRiskThreshold:
|
|
19008
|
+
mediumRiskThreshold: DEFAULT_PI_MEDIUM_RISK_THRESHOLD,
|
|
18661
19009
|
logger
|
|
18662
19010
|
});
|
|
18663
19011
|
}
|
|
@@ -18682,6 +19030,19 @@ function kv(key, value) {
|
|
|
18682
19030
|
function separatorLine(headerLength) {
|
|
18683
19031
|
return "\u2501".repeat(headerLength);
|
|
18684
19032
|
}
|
|
19033
|
+
function ruleLabel(verdict) {
|
|
19034
|
+
if (!verdict.matchedThreatId)
|
|
19035
|
+
return null;
|
|
19036
|
+
return verdict.source === "exception" ? `${verdict.matchedThreatId} (your exception rule)` : verdict.matchedThreatId;
|
|
19037
|
+
}
|
|
19038
|
+
function remediationHint(verdict) {
|
|
19039
|
+
if (verdict.source === "exception") {
|
|
19040
|
+
const id = verdict.matchedThreatId ? ` (id ${verdict.matchedThreatId})` : "";
|
|
19041
|
+
return `This block comes from your own rule in ~/.sage/exceptions.json${id} \u2014 edit or remove it there.`;
|
|
19042
|
+
}
|
|
19043
|
+
const rule = verdict.matchedThreatId ? ` (rule ${verdict.matchedThreatId})` : "";
|
|
19044
|
+
return `If this is a false positive, use the sage_report_false_positive MCP tool to report it${rule}.`;
|
|
19045
|
+
}
|
|
18685
19046
|
function formatUpdateNotice(result) {
|
|
18686
19047
|
return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/gendigitalinc/sage)`;
|
|
18687
19048
|
}
|
|
@@ -18828,15 +19189,225 @@ function summarizeArtifacts(artifacts) {
|
|
|
18828
19189
|
function formatDenyMessage(verdict, branding2 = defaultBranding) {
|
|
18829
19190
|
const maxReasons = 5;
|
|
18830
19191
|
const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, maxReasons).join("; ") + (verdict.reasons.length > maxReasons ? `; ... and ${verdict.reasons.length - maxReasons} more` : "") : verdict.category;
|
|
19192
|
+
const rule = ruleLabel(verdict);
|
|
18831
19193
|
return [
|
|
18832
19194
|
`${branding2.name} blocked this action.`,
|
|
18833
19195
|
`Severity: ${verdict.severity}`,
|
|
18834
19196
|
`Category: ${verdict.category}`,
|
|
19197
|
+
...rule ? [`Rule: ${rule}`] : [],
|
|
18835
19198
|
`Reason: ${reasons}`,
|
|
18836
|
-
|
|
19199
|
+
remediationHint(verdict)
|
|
18837
19200
|
].join("\n");
|
|
18838
19201
|
}
|
|
18839
19202
|
|
|
19203
|
+
// ../core/dist/install-state.js
|
|
19204
|
+
var import_node_path18 = require("node:path");
|
|
19205
|
+
init_config();
|
|
19206
|
+
init_file_utils();
|
|
19207
|
+
|
|
19208
|
+
// ../core/dist/notices.js
|
|
19209
|
+
init_config();
|
|
19210
|
+
var SKILL_UPLOAD_NOTICE = {
|
|
19211
|
+
id: "skill_upload_v1",
|
|
19212
|
+
body: () => [
|
|
19213
|
+
"unknown skills \u2014 each skill's SKILL.md and every supporting file in its",
|
|
19214
|
+
"folder \u2014 will be uploaded and checked against potential malicious artifacts",
|
|
19215
|
+
"starting next session. To keep skill content on this machine, set",
|
|
19216
|
+
'"skill_check": { "upload_enabled": false } in ~/.sage/config.json.'
|
|
19217
|
+
],
|
|
19218
|
+
// Suppressed once the user has explicitly set upload_enabled either way: an
|
|
19219
|
+
// explicit choice needs no consent prompt (the rollout obeys it directly).
|
|
19220
|
+
isEligible: async ({ configPath, logger }) => !(await readExplicitSkillUploadEnabled(configPath, logger)).present
|
|
19221
|
+
};
|
|
19222
|
+
var NOTICES = {
|
|
19223
|
+
[SKILL_UPLOAD_NOTICE.id]: SKILL_UPLOAD_NOTICE
|
|
19224
|
+
};
|
|
19225
|
+
var INFO_ICON = "\u2139\uFE0F";
|
|
19226
|
+
var CONTINUATION_INDENT = " ";
|
|
19227
|
+
function formatNotice(notice, branding2 = defaultBranding) {
|
|
19228
|
+
const [first = "", ...rest] = notice.body(branding2);
|
|
19229
|
+
const head = `${INFO_ICON} ${branding2.name}: ${first}`;
|
|
19230
|
+
return [head, ...rest.map((line) => `${CONTINUATION_INDENT}${line}`)].join("\n");
|
|
19231
|
+
}
|
|
19232
|
+
function formatNoticeById(id, branding2 = defaultBranding) {
|
|
19233
|
+
const notice = NOTICES[id];
|
|
19234
|
+
return notice ? formatNotice(notice, branding2) : "";
|
|
19235
|
+
}
|
|
19236
|
+
|
|
19237
|
+
// ../core/dist/install-state.js
|
|
19238
|
+
init_types2();
|
|
19239
|
+
var INSTALL_STATE_SCHEMA_VERSION = 1;
|
|
19240
|
+
var SKILL_UPLOAD_NOTICE_ID = SKILL_UPLOAD_NOTICE.id;
|
|
19241
|
+
function defaultStatePath(sageDirPath) {
|
|
19242
|
+
return (0, import_node_path18.join)(sageDirPath ?? resolvePath(SAGE_DIR), "install-state.json");
|
|
19243
|
+
}
|
|
19244
|
+
function emptyState() {
|
|
19245
|
+
return { schemaVersion: INSTALL_STATE_SCHEMA_VERSION, notices: {} };
|
|
19246
|
+
}
|
|
19247
|
+
async function loadInstallState(sageDirPath, logger = nullLogger) {
|
|
19248
|
+
let raw;
|
|
19249
|
+
try {
|
|
19250
|
+
raw = await getFileContent(defaultStatePath(sageDirPath));
|
|
19251
|
+
} catch {
|
|
19252
|
+
return emptyState();
|
|
19253
|
+
}
|
|
19254
|
+
try {
|
|
19255
|
+
const data = JSON.parse(raw);
|
|
19256
|
+
const rawNotices = data.notices ?? {};
|
|
19257
|
+
const notices = {};
|
|
19258
|
+
for (const [id, rec] of Object.entries(rawNotices)) {
|
|
19259
|
+
notices[id] = {
|
|
19260
|
+
noticed: rec.noticed === true,
|
|
19261
|
+
version: typeof rec.version === "string" ? rec.version : void 0
|
|
19262
|
+
};
|
|
19263
|
+
}
|
|
19264
|
+
return {
|
|
19265
|
+
schemaVersion: typeof data.schema_version === "number" ? data.schema_version : INSTALL_STATE_SCHEMA_VERSION,
|
|
19266
|
+
lastRunVersion: typeof data.last_run_version === "string" ? data.last_run_version : void 0,
|
|
19267
|
+
notices
|
|
19268
|
+
};
|
|
19269
|
+
} catch (e) {
|
|
19270
|
+
logger.warn("Failed to parse install state; treating as fresh", { error: String(e) });
|
|
19271
|
+
return emptyState();
|
|
19272
|
+
}
|
|
19273
|
+
}
|
|
19274
|
+
async function saveInstallState(state, sageDirPath, logger = nullLogger) {
|
|
19275
|
+
try {
|
|
19276
|
+
const data = {
|
|
19277
|
+
schema_version: state.schemaVersion,
|
|
19278
|
+
last_run_version: state.lastRunVersion,
|
|
19279
|
+
notices: Object.fromEntries(Object.entries(state.notices).map(([id, rec]) => [
|
|
19280
|
+
id,
|
|
19281
|
+
{ noticed: rec.noticed, version: rec.version }
|
|
19282
|
+
]))
|
|
19283
|
+
};
|
|
19284
|
+
await atomicWriteJson(defaultStatePath(sageDirPath), data);
|
|
19285
|
+
} catch (e) {
|
|
19286
|
+
logger.warn("Failed to save install state", { error: String(e) });
|
|
19287
|
+
}
|
|
19288
|
+
}
|
|
19289
|
+
function decideSkillUploadRollout(state, opts = {}) {
|
|
19290
|
+
const versionChanged = opts.version !== void 0 && state.lastRunVersion !== opts.version;
|
|
19291
|
+
const withVersion = () => ({ ...state, lastRunVersion: opts.version });
|
|
19292
|
+
if (opts.explicitConfig) {
|
|
19293
|
+
return {
|
|
19294
|
+
uploadActive: opts.explicitValue === true,
|
|
19295
|
+
showNotice: false,
|
|
19296
|
+
nextState: versionChanged ? withVersion() : null
|
|
19297
|
+
};
|
|
19298
|
+
}
|
|
19299
|
+
const notice = state.notices[SKILL_UPLOAD_NOTICE_ID];
|
|
19300
|
+
if (notice?.noticed === true) {
|
|
19301
|
+
return {
|
|
19302
|
+
uploadActive: true,
|
|
19303
|
+
showNotice: false,
|
|
19304
|
+
nextState: versionChanged ? withVersion() : null
|
|
19305
|
+
};
|
|
19306
|
+
}
|
|
19307
|
+
const nextState = {
|
|
19308
|
+
...state,
|
|
19309
|
+
lastRunVersion: opts.version ?? state.lastRunVersion,
|
|
19310
|
+
notices: {
|
|
19311
|
+
...state.notices,
|
|
19312
|
+
[SKILL_UPLOAD_NOTICE_ID]: { noticed: true, version: opts.version }
|
|
19313
|
+
}
|
|
19314
|
+
};
|
|
19315
|
+
return { uploadActive: false, showNotice: true, nextState };
|
|
19316
|
+
}
|
|
19317
|
+
var processRollout;
|
|
19318
|
+
async function resolveSkillUploadRollout(args) {
|
|
19319
|
+
const commitNotice = args.commitNotice ?? true;
|
|
19320
|
+
const logger = args.logger ?? nullLogger;
|
|
19321
|
+
let explicit;
|
|
19322
|
+
try {
|
|
19323
|
+
explicit = await readExplicitSkillUploadEnabled(args.configPath, logger);
|
|
19324
|
+
} catch {
|
|
19325
|
+
explicit = { present: false, value: false };
|
|
19326
|
+
}
|
|
19327
|
+
if (explicit.present) {
|
|
19328
|
+
const resolved = {
|
|
19329
|
+
uploadActive: explicit.value === true,
|
|
19330
|
+
showNotice: false,
|
|
19331
|
+
deferUnknownSkills: false
|
|
19332
|
+
};
|
|
19333
|
+
logger.debug("Skill-upload rollout resolved", {
|
|
19334
|
+
explicitConfig: true,
|
|
19335
|
+
explicitValue: explicit.value,
|
|
19336
|
+
uploadActive: resolved.uploadActive,
|
|
19337
|
+
showNotice: false,
|
|
19338
|
+
deferUnknownSkills: false,
|
|
19339
|
+
commitNotice,
|
|
19340
|
+
statePersisted: false,
|
|
19341
|
+
version: args.version
|
|
19342
|
+
});
|
|
19343
|
+
return resolved;
|
|
19344
|
+
}
|
|
19345
|
+
if (processRollout)
|
|
19346
|
+
return processRollout;
|
|
19347
|
+
try {
|
|
19348
|
+
const state = await loadInstallState(args.sageDirPath, logger);
|
|
19349
|
+
const decision = decideSkillUploadRollout(state, { version: args.version });
|
|
19350
|
+
if (commitNotice) {
|
|
19351
|
+
if (decision.nextState) {
|
|
19352
|
+
await saveInstallState(decision.nextState, args.sageDirPath, logger);
|
|
19353
|
+
}
|
|
19354
|
+
processRollout = {
|
|
19355
|
+
uploadActive: decision.uploadActive,
|
|
19356
|
+
showNotice: decision.showNotice,
|
|
19357
|
+
deferUnknownSkills: decision.showNotice
|
|
19358
|
+
};
|
|
19359
|
+
} else {
|
|
19360
|
+
processRollout = {
|
|
19361
|
+
uploadActive: decision.uploadActive,
|
|
19362
|
+
showNotice: false,
|
|
19363
|
+
deferUnknownSkills: decision.showNotice
|
|
19364
|
+
};
|
|
19365
|
+
}
|
|
19366
|
+
logger.debug("Skill-upload rollout resolved", {
|
|
19367
|
+
explicitConfig: false,
|
|
19368
|
+
uploadActive: processRollout.uploadActive,
|
|
19369
|
+
showNotice: processRollout.showNotice,
|
|
19370
|
+
deferUnknownSkills: processRollout.deferUnknownSkills,
|
|
19371
|
+
commitNotice,
|
|
19372
|
+
statePersisted: commitNotice && decision.nextState !== null,
|
|
19373
|
+
version: args.version
|
|
19374
|
+
});
|
|
19375
|
+
} catch (e) {
|
|
19376
|
+
logger.warn("Skill-upload rollout resolution failed; suppressing upload", {
|
|
19377
|
+
error: String(e)
|
|
19378
|
+
});
|
|
19379
|
+
processRollout = { uploadActive: false, showNotice: commitNotice, deferUnknownSkills: true };
|
|
19380
|
+
}
|
|
19381
|
+
return processRollout;
|
|
19382
|
+
}
|
|
19383
|
+
async function takePendingNotices(args) {
|
|
19384
|
+
const logger = args.logger ?? nullLogger;
|
|
19385
|
+
try {
|
|
19386
|
+
const state = await loadInstallState(args.sageDirPath, logger);
|
|
19387
|
+
const toShow = [];
|
|
19388
|
+
for (const notice of Object.values(NOTICES)) {
|
|
19389
|
+
if (state.notices[notice.id]?.noticed === true)
|
|
19390
|
+
continue;
|
|
19391
|
+
const eligible = notice.isEligible ? await notice.isEligible({ configPath: args.configPath, logger }) : true;
|
|
19392
|
+
if (eligible)
|
|
19393
|
+
toShow.push(notice.id);
|
|
19394
|
+
}
|
|
19395
|
+
if (toShow.length === 0)
|
|
19396
|
+
return [];
|
|
19397
|
+
const nextNotices = { ...state.notices };
|
|
19398
|
+
for (const id of toShow)
|
|
19399
|
+
nextNotices[id] = { noticed: true };
|
|
19400
|
+
await saveInstallState({ ...state, notices: nextNotices }, args.sageDirPath, logger);
|
|
19401
|
+
logger.debug("Pending notices taken for delivery", { noticeIds: toShow });
|
|
19402
|
+
return toShow;
|
|
19403
|
+
} catch (e) {
|
|
19404
|
+
logger.warn("takePendingNotices failed; showing no notices this turn", {
|
|
19405
|
+
error: String(e)
|
|
19406
|
+
});
|
|
19407
|
+
return [];
|
|
19408
|
+
}
|
|
19409
|
+
}
|
|
19410
|
+
|
|
18840
19411
|
// ../core/dist/model-download.js
|
|
18841
19412
|
init_types2();
|
|
18842
19413
|
|
|
@@ -18908,8 +19479,8 @@ function createOperationalLogger(config2, runtime) {
|
|
|
18908
19479
|
try {
|
|
18909
19480
|
const result = await Promise.race([
|
|
18910
19481
|
Promise.allSettled([...pendingWrites]).then(() => "drained"),
|
|
18911
|
-
new Promise((
|
|
18912
|
-
timeout = setTimeout(() =>
|
|
19482
|
+
new Promise((resolve10) => {
|
|
19483
|
+
timeout = setTimeout(() => resolve10("timeout"), remainingMs);
|
|
18913
19484
|
timeout.unref?.();
|
|
18914
19485
|
})
|
|
18915
19486
|
]);
|
|
@@ -18958,13 +19529,14 @@ function createOperationalLogger(config2, runtime) {
|
|
|
18958
19529
|
|
|
18959
19530
|
// ../core/dist/plugin-scan-cache.js
|
|
18960
19531
|
var import_node_crypto7 = require("node:crypto");
|
|
18961
|
-
var
|
|
18962
|
-
var
|
|
19532
|
+
var import_promises8 = require("node:fs/promises");
|
|
19533
|
+
var import_node_path19 = require("node:path");
|
|
19534
|
+
init_config();
|
|
18963
19535
|
init_file_utils();
|
|
18964
19536
|
init_types2();
|
|
18965
|
-
var DEFAULT_CACHE_PATH = (0,
|
|
18966
|
-
var
|
|
18967
|
-
var SCHEMA_VERSION =
|
|
19537
|
+
var DEFAULT_CACHE_PATH = (0, import_node_path19.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
|
|
19538
|
+
var DEFAULT_CACHE_TTL_MS = MS_PER_DAY;
|
|
19539
|
+
var SCHEMA_VERSION = 5;
|
|
18968
19540
|
function cacheKey(pluginKey, version, lastUpdated) {
|
|
18969
19541
|
return `${pluginKey}:${version}:${lastUpdated}`;
|
|
18970
19542
|
}
|
|
@@ -18975,13 +19547,13 @@ async function computeConfigHash(sageVersion, ...dirs) {
|
|
|
18975
19547
|
for (const dir of dirs) {
|
|
18976
19548
|
let files;
|
|
18977
19549
|
try {
|
|
18978
|
-
files = (await (0,
|
|
19550
|
+
files = (await (0, import_promises8.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
18979
19551
|
} catch {
|
|
18980
19552
|
continue;
|
|
18981
19553
|
}
|
|
18982
19554
|
for (const file of files) {
|
|
18983
19555
|
try {
|
|
18984
|
-
const content = await getFileContent((0,
|
|
19556
|
+
const content = await getFileContent((0, import_node_path19.join)(dir, file));
|
|
18985
19557
|
h.update(content);
|
|
18986
19558
|
} catch {
|
|
18987
19559
|
}
|
|
@@ -19050,7 +19622,7 @@ async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nu
|
|
|
19050
19622
|
logger.warn(`Failed to save scan cache to ${cachePath}`, { error: String(e) });
|
|
19051
19623
|
}
|
|
19052
19624
|
}
|
|
19053
|
-
function isCached(cache2, pluginKey, version, lastUpdated) {
|
|
19625
|
+
function isCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
|
|
19054
19626
|
const key = cacheKey(pluginKey, version, lastUpdated);
|
|
19055
19627
|
const entry = cache2.entries[key];
|
|
19056
19628
|
if (!entry)
|
|
@@ -19058,13 +19630,13 @@ function isCached(cache2, pluginKey, version, lastUpdated) {
|
|
|
19058
19630
|
try {
|
|
19059
19631
|
const scannedAt = new Date(entry.scannedAt);
|
|
19060
19632
|
const age = Date.now() - scannedAt.getTime();
|
|
19061
|
-
return age <
|
|
19633
|
+
return age < ttlMs;
|
|
19062
19634
|
} catch {
|
|
19063
19635
|
return false;
|
|
19064
19636
|
}
|
|
19065
19637
|
}
|
|
19066
|
-
function getCached(cache2, pluginKey, version, lastUpdated) {
|
|
19067
|
-
if (!isCached(cache2, pluginKey, version, lastUpdated))
|
|
19638
|
+
function getCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
|
|
19639
|
+
if (!isCached(cache2, pluginKey, version, lastUpdated, ttlMs))
|
|
19068
19640
|
return null;
|
|
19069
19641
|
const key = cacheKey(pluginKey, version, lastUpdated);
|
|
19070
19642
|
return cache2.entries[key] ?? null;
|
|
@@ -19081,18 +19653,21 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
|
|
|
19081
19653
|
|
|
19082
19654
|
// ../core/dist/plugin-scanner.js
|
|
19083
19655
|
var import_node_crypto9 = require("node:crypto");
|
|
19084
|
-
var
|
|
19085
|
-
var
|
|
19656
|
+
var import_promises11 = require("node:fs/promises");
|
|
19657
|
+
var import_node_path23 = require("node:path");
|
|
19086
19658
|
init_config();
|
|
19087
19659
|
init_file_utils();
|
|
19088
19660
|
|
|
19089
19661
|
// ../core/dist/skill-id.js
|
|
19090
19662
|
var import_node_crypto8 = require("node:crypto");
|
|
19091
|
-
var
|
|
19092
|
-
var
|
|
19663
|
+
var import_promises9 = require("node:fs/promises");
|
|
19664
|
+
var import_node_path20 = require("node:path");
|
|
19093
19665
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
|
|
19666
|
+
var MAX_SKILL_BYTES = 50 * 1024 * 1024;
|
|
19667
|
+
var SkillTooLargeError = class extends Error {
|
|
19668
|
+
};
|
|
19094
19669
|
function isContained(childReal, rootReal) {
|
|
19095
|
-
return childReal === rootReal || childReal.startsWith(`${rootReal}${
|
|
19670
|
+
return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path20.sep}`);
|
|
19096
19671
|
}
|
|
19097
19672
|
function normalizePath(p) {
|
|
19098
19673
|
let out = p.replace(/\\/g, "/").normalize("NFC");
|
|
@@ -19102,47 +19677,51 @@ function normalizePath(p) {
|
|
|
19102
19677
|
}
|
|
19103
19678
|
return out;
|
|
19104
19679
|
}
|
|
19105
|
-
async function entriesFromDirectory(dirPath) {
|
|
19106
|
-
const absDir = (0,
|
|
19107
|
-
const rootReal = await (0,
|
|
19680
|
+
async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY) {
|
|
19681
|
+
const absDir = (0, import_node_path20.resolve)(dirPath);
|
|
19682
|
+
const rootReal = await (0, import_promises9.realpath)(absDir);
|
|
19108
19683
|
const entries = [];
|
|
19109
19684
|
const visited = /* @__PURE__ */ new Set();
|
|
19685
|
+
let total = 0;
|
|
19110
19686
|
async function walk(currentPath) {
|
|
19111
|
-
const real = await (0,
|
|
19687
|
+
const real = await (0, import_promises9.realpath)(currentPath);
|
|
19112
19688
|
if (visited.has(real))
|
|
19113
19689
|
return;
|
|
19114
19690
|
if (!isContained(real, rootReal))
|
|
19115
19691
|
return;
|
|
19116
19692
|
visited.add(real);
|
|
19117
|
-
const items = await (0,
|
|
19693
|
+
const items = await (0, import_promises9.readdir)(currentPath);
|
|
19118
19694
|
const dirs = [];
|
|
19119
19695
|
const files = [];
|
|
19120
19696
|
for (const item of items) {
|
|
19121
|
-
const fullPath = (0,
|
|
19122
|
-
const st = await (0,
|
|
19697
|
+
const fullPath = (0, import_node_path20.join)(currentPath, item);
|
|
19698
|
+
const st = await (0, import_promises9.stat)(fullPath);
|
|
19123
19699
|
if (st.isDirectory())
|
|
19124
19700
|
dirs.push(item);
|
|
19125
19701
|
else if (st.isFile())
|
|
19126
|
-
files.push(item);
|
|
19702
|
+
files.push({ name: item, size: st.size });
|
|
19127
19703
|
}
|
|
19128
19704
|
dirs.sort();
|
|
19129
|
-
files.sort();
|
|
19705
|
+
files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
19130
19706
|
for (const d of dirs) {
|
|
19131
|
-
const fullPath = (0,
|
|
19132
|
-
const relPath = (0,
|
|
19707
|
+
const fullPath = (0, import_node_path20.join)(currentPath, d);
|
|
19708
|
+
const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19133
19709
|
entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
|
|
19134
19710
|
await walk(fullPath);
|
|
19135
19711
|
}
|
|
19136
19712
|
for (const f of files) {
|
|
19137
|
-
const fullPath = (0,
|
|
19138
|
-
const fileReal = await (0,
|
|
19713
|
+
const fullPath = (0, import_node_path20.join)(currentPath, f.name);
|
|
19714
|
+
const fileReal = await (0, import_promises9.realpath)(fullPath);
|
|
19139
19715
|
if (!isContained(fileReal, rootReal))
|
|
19140
19716
|
continue;
|
|
19141
|
-
|
|
19717
|
+
total += f.size;
|
|
19718
|
+
if (total > maxBytes)
|
|
19719
|
+
throw new SkillTooLargeError();
|
|
19720
|
+
const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19142
19721
|
entries.push({
|
|
19143
19722
|
entryPath: relPath,
|
|
19144
19723
|
isDir: false,
|
|
19145
|
-
content: await (0,
|
|
19724
|
+
content: await (0, import_promises9.readFile)(fullPath)
|
|
19146
19725
|
});
|
|
19147
19726
|
}
|
|
19148
19727
|
}
|
|
@@ -19179,77 +19758,347 @@ function computeSkillId(entries) {
|
|
|
19179
19758
|
}
|
|
19180
19759
|
return { skillId: skillHasher.digest("hex"), fileHashes };
|
|
19181
19760
|
}
|
|
19182
|
-
async function
|
|
19761
|
+
async function findSkillPackagesWithMtime(rootDir) {
|
|
19183
19762
|
const found = [];
|
|
19184
19763
|
let rootReal;
|
|
19185
19764
|
try {
|
|
19186
|
-
|
|
19187
|
-
if (!rootStat.isDirectory())
|
|
19188
|
-
return found;
|
|
19189
|
-
rootReal = await (0, import_promises8.realpath)((0, import_node_path17.resolve)(rootDir));
|
|
19765
|
+
rootReal = await (0, import_promises9.realpath)((0, import_node_path20.resolve)(rootDir));
|
|
19190
19766
|
} catch {
|
|
19191
19767
|
return found;
|
|
19192
19768
|
}
|
|
19193
19769
|
const visited = /* @__PURE__ */ new Set();
|
|
19194
|
-
async function walk(
|
|
19770
|
+
async function walk(currentPath) {
|
|
19195
19771
|
let real;
|
|
19196
19772
|
try {
|
|
19197
|
-
real = await (0,
|
|
19773
|
+
real = await (0, import_promises9.realpath)(currentPath);
|
|
19198
19774
|
} catch {
|
|
19199
|
-
return;
|
|
19775
|
+
return 0;
|
|
19200
19776
|
}
|
|
19201
19777
|
if (visited.has(real))
|
|
19202
|
-
return;
|
|
19778
|
+
return 0;
|
|
19203
19779
|
if (!isContained(real, rootReal))
|
|
19204
|
-
return;
|
|
19780
|
+
return 0;
|
|
19205
19781
|
visited.add(real);
|
|
19782
|
+
let st;
|
|
19783
|
+
try {
|
|
19784
|
+
st = await (0, import_promises9.stat)(currentPath);
|
|
19785
|
+
} catch {
|
|
19786
|
+
return 0;
|
|
19787
|
+
}
|
|
19788
|
+
let subtreeMax = st.mtimeMs;
|
|
19789
|
+
if (!st.isDirectory())
|
|
19790
|
+
return subtreeMax;
|
|
19206
19791
|
let items;
|
|
19207
19792
|
try {
|
|
19208
|
-
items = await (0,
|
|
19793
|
+
items = await (0, import_promises9.readdir)(currentPath);
|
|
19209
19794
|
} catch {
|
|
19210
|
-
return;
|
|
19795
|
+
return subtreeMax;
|
|
19211
19796
|
}
|
|
19212
19797
|
items.sort();
|
|
19798
|
+
let entry;
|
|
19213
19799
|
if (items.includes("SKILL.md")) {
|
|
19214
19800
|
try {
|
|
19215
|
-
|
|
19216
|
-
|
|
19217
|
-
found.push(
|
|
19801
|
+
if ((await (0, import_promises9.stat)((0, import_node_path20.join)(currentPath, "SKILL.md"))).isFile()) {
|
|
19802
|
+
entry = { folder: currentPath, newestMtimeMs: 0 };
|
|
19803
|
+
found.push(entry);
|
|
19804
|
+
}
|
|
19218
19805
|
} catch {
|
|
19219
19806
|
}
|
|
19220
19807
|
}
|
|
19221
19808
|
for (const item of items) {
|
|
19222
19809
|
if (SKIP_DIRS.has(item))
|
|
19223
19810
|
continue;
|
|
19224
|
-
const
|
|
19225
|
-
|
|
19226
|
-
|
|
19227
|
-
st = await (0, import_promises8.stat)(fullPath);
|
|
19228
|
-
} catch {
|
|
19229
|
-
continue;
|
|
19230
|
-
}
|
|
19231
|
-
if (st.isDirectory()) {
|
|
19232
|
-
await walk(fullPath);
|
|
19233
|
-
}
|
|
19811
|
+
const childMax = await walk((0, import_node_path20.join)(currentPath, item));
|
|
19812
|
+
if (childMax > subtreeMax)
|
|
19813
|
+
subtreeMax = childMax;
|
|
19234
19814
|
}
|
|
19815
|
+
if (entry)
|
|
19816
|
+
entry.newestMtimeMs = subtreeMax;
|
|
19817
|
+
return subtreeMax;
|
|
19235
19818
|
}
|
|
19236
|
-
await walk((0,
|
|
19819
|
+
await walk((0, import_node_path20.resolve)(rootDir));
|
|
19237
19820
|
return found;
|
|
19238
19821
|
}
|
|
19239
|
-
|
|
19240
|
-
const
|
|
19822
|
+
function looseSkillKey(skillsRoot, folder, tag, scope) {
|
|
19823
|
+
const rel = (0, import_node_path20.relative)((0, import_node_path20.resolve)(skillsRoot), (0, import_node_path20.resolve)(folder)).split(import_node_path20.sep).join("/") || (0, import_node_path20.basename)(folder);
|
|
19824
|
+
return `skill:${tag}/${rel}@${scope}`;
|
|
19825
|
+
}
|
|
19826
|
+
async function discoverLooseSkills(skillsDir, tag, scope) {
|
|
19827
|
+
const resolvedRoot = (0, import_node_path20.resolve)(skillsDir);
|
|
19828
|
+
const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0, import_node_path20.resolve)(p.folder) !== resolvedRoot);
|
|
19829
|
+
return packages.map(({ folder, newestMtimeMs }) => ({
|
|
19830
|
+
key: looseSkillKey(resolvedRoot, folder, tag, scope),
|
|
19831
|
+
installPath: folder,
|
|
19832
|
+
version: scope,
|
|
19833
|
+
lastUpdated: new Date(newestMtimeMs).toISOString()
|
|
19834
|
+
}));
|
|
19835
|
+
}
|
|
19836
|
+
async function dedupeSkillsByResolvedPath(skills) {
|
|
19837
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19838
|
+
const out = [];
|
|
19839
|
+
for (const skill of skills) {
|
|
19840
|
+
let canonical = skill.installPath;
|
|
19841
|
+
try {
|
|
19842
|
+
canonical = await (0, import_promises9.realpath)(skill.installPath);
|
|
19843
|
+
} catch {
|
|
19844
|
+
}
|
|
19845
|
+
if (seen.has(canonical))
|
|
19846
|
+
continue;
|
|
19847
|
+
seen.add(canonical);
|
|
19848
|
+
out.push(skill);
|
|
19849
|
+
}
|
|
19850
|
+
return out;
|
|
19851
|
+
}
|
|
19852
|
+
async function discoverLooseSkillsAcrossRoots(roots) {
|
|
19853
|
+
const lists = await Promise.all(roots.map(async ({ dir, tag, scope }) => {
|
|
19854
|
+
try {
|
|
19855
|
+
return await discoverLooseSkills(dir, tag, scope);
|
|
19856
|
+
} catch {
|
|
19857
|
+
return [];
|
|
19858
|
+
}
|
|
19859
|
+
}));
|
|
19860
|
+
return lists.length <= 1 ? lists[0] ?? [] : dedupeSkillsByResolvedPath(lists.flat());
|
|
19861
|
+
}
|
|
19862
|
+
async function computeSkillIdsForRoot(rootDir, logger) {
|
|
19863
|
+
const folders = (await findSkillPackagesWithMtime(rootDir)).map((p) => p.folder);
|
|
19241
19864
|
const out = [];
|
|
19242
19865
|
for (const folder of folders) {
|
|
19243
19866
|
try {
|
|
19244
|
-
const entries = await entriesFromDirectory(folder);
|
|
19867
|
+
const entries = await entriesFromDirectory(folder, MAX_SKILL_BYTES);
|
|
19868
|
+
if (entries.length === 0) {
|
|
19869
|
+
logger?.warn("Skill enumerates to no entries; not analyzable, skipping", { folder });
|
|
19870
|
+
continue;
|
|
19871
|
+
}
|
|
19245
19872
|
const { skillId } = computeSkillId(entries);
|
|
19246
19873
|
out.push({ folder, skillId });
|
|
19247
|
-
} catch {
|
|
19874
|
+
} catch (e) {
|
|
19875
|
+
if (e instanceof SkillTooLargeError) {
|
|
19876
|
+
logger?.warn("Skill directory exceeds 50 MB limit; skipping", { folder });
|
|
19877
|
+
}
|
|
19248
19878
|
}
|
|
19249
19879
|
}
|
|
19250
19880
|
return out;
|
|
19251
19881
|
}
|
|
19252
19882
|
|
|
19883
|
+
// ../core/dist/skill-pending.js
|
|
19884
|
+
var import_promises10 = require("node:fs/promises");
|
|
19885
|
+
var import_node_path21 = require("node:path");
|
|
19886
|
+
init_config();
|
|
19887
|
+
init_file_utils();
|
|
19888
|
+
init_types2();
|
|
19889
|
+
var SCHEMA_VERSION2 = 1;
|
|
19890
|
+
var PENDING_TTL_MS = 60 * 60 * 1e3;
|
|
19891
|
+
function defaultPendingPath() {
|
|
19892
|
+
return (0, import_node_path21.join)(resolvePath(SAGE_DIR), "skill_pending.json");
|
|
19893
|
+
}
|
|
19894
|
+
async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19895
|
+
let raw;
|
|
19896
|
+
try {
|
|
19897
|
+
raw = await getFileContent(pendingPath);
|
|
19898
|
+
} catch {
|
|
19899
|
+
return { entries: {} };
|
|
19900
|
+
}
|
|
19901
|
+
try {
|
|
19902
|
+
const data = JSON.parse(raw);
|
|
19903
|
+
const rawEntries = data.entries ?? {};
|
|
19904
|
+
const now = Date.now();
|
|
19905
|
+
const entries = {};
|
|
19906
|
+
for (const [skillId, entry] of Object.entries(rawEntries)) {
|
|
19907
|
+
const folder = entry.folder;
|
|
19908
|
+
const submittedAt = entry.submitted_at;
|
|
19909
|
+
if (!folder || !submittedAt)
|
|
19910
|
+
continue;
|
|
19911
|
+
const ts = Date.parse(submittedAt);
|
|
19912
|
+
if (!isFreshTimestamp(ts, PENDING_TTL_MS, now))
|
|
19913
|
+
continue;
|
|
19914
|
+
entries[skillId] = {
|
|
19915
|
+
folder,
|
|
19916
|
+
submittedAt,
|
|
19917
|
+
agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
|
|
19918
|
+
containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
|
|
19919
|
+
};
|
|
19920
|
+
}
|
|
19921
|
+
return { entries };
|
|
19922
|
+
} catch (e) {
|
|
19923
|
+
logger.warn(`Failed to load pending marker from ${pendingPath}`, { error: String(e) });
|
|
19924
|
+
return { entries: {} };
|
|
19925
|
+
}
|
|
19926
|
+
}
|
|
19927
|
+
async function savePendingMarker(marker, pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19928
|
+
if (Object.keys(marker.entries).length === 0) {
|
|
19929
|
+
try {
|
|
19930
|
+
await (0, import_promises10.unlink)(pendingPath);
|
|
19931
|
+
} catch (e) {
|
|
19932
|
+
if (!e || typeof e !== "object" || e.code !== "ENOENT") {
|
|
19933
|
+
logger.warn(`Failed to delete empty pending marker at ${pendingPath}`, {
|
|
19934
|
+
error: String(e)
|
|
19935
|
+
});
|
|
19936
|
+
}
|
|
19937
|
+
}
|
|
19938
|
+
return;
|
|
19939
|
+
}
|
|
19940
|
+
try {
|
|
19941
|
+
const data = {
|
|
19942
|
+
schema_version: SCHEMA_VERSION2,
|
|
19943
|
+
entries: Object.fromEntries(Object.entries(marker.entries).map(([skillId, entry]) => [
|
|
19944
|
+
skillId,
|
|
19945
|
+
{
|
|
19946
|
+
folder: entry.folder,
|
|
19947
|
+
submitted_at: entry.submittedAt,
|
|
19948
|
+
agent_runtime: entry.agentRuntime,
|
|
19949
|
+
container_key: entry.containerKey
|
|
19950
|
+
}
|
|
19951
|
+
]))
|
|
19952
|
+
};
|
|
19953
|
+
await atomicWriteJson(pendingPath, data);
|
|
19954
|
+
} catch (e) {
|
|
19955
|
+
logger.warn(`Failed to save pending marker to ${pendingPath}`, { error: String(e) });
|
|
19956
|
+
}
|
|
19957
|
+
}
|
|
19958
|
+
function isPending(marker, skillId) {
|
|
19959
|
+
return skillId in marker.entries;
|
|
19960
|
+
}
|
|
19961
|
+
function addPending(marker, skillId, folder, origin = {}) {
|
|
19962
|
+
marker.entries[skillId] = {
|
|
19963
|
+
folder,
|
|
19964
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19965
|
+
agentRuntime: origin.agentRuntime,
|
|
19966
|
+
containerKey: origin.containerKey
|
|
19967
|
+
};
|
|
19968
|
+
}
|
|
19969
|
+
function removePending(marker, skillId) {
|
|
19970
|
+
delete marker.entries[skillId];
|
|
19971
|
+
}
|
|
19972
|
+
function listPending(marker) {
|
|
19973
|
+
return Object.entries(marker.entries).map(([skillId, entry]) => ({
|
|
19974
|
+
skillId,
|
|
19975
|
+
folder: entry.folder,
|
|
19976
|
+
agentRuntime: entry.agentRuntime,
|
|
19977
|
+
containerKey: entry.containerKey
|
|
19978
|
+
}));
|
|
19979
|
+
}
|
|
19980
|
+
|
|
19981
|
+
// ../core/dist/skill-verdict-cache.js
|
|
19982
|
+
var import_node_path22 = require("node:path");
|
|
19983
|
+
init_config();
|
|
19984
|
+
init_file_utils();
|
|
19985
|
+
init_types2();
|
|
19986
|
+
var SCHEMA_VERSION3 = 1;
|
|
19987
|
+
var DEFAULT_VERDICT_TTL_MS = MS_PER_DAY;
|
|
19988
|
+
function parseSources(raw) {
|
|
19989
|
+
if (!Array.isArray(raw))
|
|
19990
|
+
return void 0;
|
|
19991
|
+
const sources = [];
|
|
19992
|
+
for (const item of raw) {
|
|
19993
|
+
if (!item || typeof item !== "object")
|
|
19994
|
+
continue;
|
|
19995
|
+
const entry = item;
|
|
19996
|
+
const source = {
|
|
19997
|
+
agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
|
|
19998
|
+
containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
|
|
19999
|
+
};
|
|
20000
|
+
if (source.agentRuntime || source.containerKey)
|
|
20001
|
+
sources.push(source);
|
|
20002
|
+
}
|
|
20003
|
+
return sources.length > 0 ? sources : void 0;
|
|
20004
|
+
}
|
|
20005
|
+
function mergeSources(existing, incoming) {
|
|
20006
|
+
const merged = [];
|
|
20007
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20008
|
+
for (const source of [...existing ?? [], ...incoming ?? []]) {
|
|
20009
|
+
const key = source.agentRuntime ?? "";
|
|
20010
|
+
if (seen.has(key))
|
|
20011
|
+
continue;
|
|
20012
|
+
seen.add(key);
|
|
20013
|
+
merged.push(source);
|
|
20014
|
+
}
|
|
20015
|
+
return merged.length > 0 ? merged : void 0;
|
|
20016
|
+
}
|
|
20017
|
+
function defaultCachePath2() {
|
|
20018
|
+
return (0, import_node_path22.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
|
|
20019
|
+
}
|
|
20020
|
+
async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger = nullLogger) {
|
|
20021
|
+
let raw;
|
|
20022
|
+
try {
|
|
20023
|
+
raw = await getFileContent(cachePath);
|
|
20024
|
+
} catch {
|
|
20025
|
+
return { entries: {} };
|
|
20026
|
+
}
|
|
20027
|
+
try {
|
|
20028
|
+
const data = JSON.parse(raw);
|
|
20029
|
+
const rawEntries = data.entries ?? {};
|
|
20030
|
+
const now = Date.now();
|
|
20031
|
+
const entries = {};
|
|
20032
|
+
let pruned = 0;
|
|
20033
|
+
for (const [skillId, entry] of Object.entries(rawEntries)) {
|
|
20034
|
+
const analyzedAt = entry.analyzed_at;
|
|
20035
|
+
if (!analyzedAt)
|
|
20036
|
+
continue;
|
|
20037
|
+
const ts = Date.parse(analyzedAt);
|
|
20038
|
+
if (!isFreshTimestamp(ts, ttlMs, now)) {
|
|
20039
|
+
pruned++;
|
|
20040
|
+
continue;
|
|
20041
|
+
}
|
|
20042
|
+
entries[skillId] = {
|
|
20043
|
+
verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
|
|
20044
|
+
summary: typeof entry.summary === "string" ? entry.summary : void 0,
|
|
20045
|
+
skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
|
|
20046
|
+
sources: parseSources(entry.sources),
|
|
20047
|
+
analyzedAt
|
|
20048
|
+
};
|
|
20049
|
+
}
|
|
20050
|
+
const cache2 = { entries };
|
|
20051
|
+
if (pruned > 0) {
|
|
20052
|
+
logger.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
|
|
20053
|
+
await saveSkillVerdictCache(cache2, cachePath, logger);
|
|
20054
|
+
}
|
|
20055
|
+
return cache2;
|
|
20056
|
+
} catch (e) {
|
|
20057
|
+
logger.warn(`Failed to load verdict cache from ${cachePath}`, { error: String(e) });
|
|
20058
|
+
return { entries: {} };
|
|
20059
|
+
}
|
|
20060
|
+
}
|
|
20061
|
+
async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger = nullLogger) {
|
|
20062
|
+
try {
|
|
20063
|
+
const data = {
|
|
20064
|
+
schema_version: SCHEMA_VERSION3,
|
|
20065
|
+
entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
|
|
20066
|
+
skillId,
|
|
20067
|
+
{
|
|
20068
|
+
verdict: v.verdict,
|
|
20069
|
+
summary: v.summary,
|
|
20070
|
+
skill_name: v.skillName,
|
|
20071
|
+
sources: v.sources?.map((s) => ({
|
|
20072
|
+
agent_runtime: s.agentRuntime,
|
|
20073
|
+
container_key: s.containerKey
|
|
20074
|
+
})),
|
|
20075
|
+
analyzed_at: v.analyzedAt
|
|
20076
|
+
}
|
|
20077
|
+
]))
|
|
20078
|
+
};
|
|
20079
|
+
await atomicWriteJson(cachePath, data);
|
|
20080
|
+
} catch (e) {
|
|
20081
|
+
logger.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
|
|
20082
|
+
}
|
|
20083
|
+
}
|
|
20084
|
+
function getVerdict(cache2, skillId, ttlMs = DEFAULT_VERDICT_TTL_MS) {
|
|
20085
|
+
const entry = cache2.entries[skillId];
|
|
20086
|
+
if (!entry)
|
|
20087
|
+
return null;
|
|
20088
|
+
const ts = Date.parse(entry.analyzedAt);
|
|
20089
|
+
if (!isFreshTimestamp(ts, ttlMs))
|
|
20090
|
+
return null;
|
|
20091
|
+
return entry;
|
|
20092
|
+
}
|
|
20093
|
+
function putVerdict(cache2, skillId, verdict) {
|
|
20094
|
+
const existing = cache2.entries[skillId];
|
|
20095
|
+
cache2.entries[skillId] = {
|
|
20096
|
+
...verdict,
|
|
20097
|
+
sources: mergeSources(existing?.sources, verdict.sources),
|
|
20098
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20099
|
+
};
|
|
20100
|
+
}
|
|
20101
|
+
|
|
19253
20102
|
// ../core/dist/plugin-scanner.js
|
|
19254
20103
|
init_types2();
|
|
19255
20104
|
var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
@@ -19274,15 +20123,34 @@ var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
|
19274
20123
|
var MAX_FILE_SIZE = 512 * 1024;
|
|
19275
20124
|
async function walkPluginFiles(installPath, logger) {
|
|
19276
20125
|
const files = [];
|
|
20126
|
+
const visited = /* @__PURE__ */ new Set();
|
|
20127
|
+
let rootReal;
|
|
20128
|
+
try {
|
|
20129
|
+
rootReal = await (0, import_promises11.realpath)((0, import_node_path23.resolve)(installPath));
|
|
20130
|
+
} catch (e) {
|
|
20131
|
+
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
20132
|
+
return files;
|
|
20133
|
+
}
|
|
19277
20134
|
async function walk(dirOrFile) {
|
|
20135
|
+
let real;
|
|
20136
|
+
try {
|
|
20137
|
+
real = await (0, import_promises11.realpath)(dirOrFile);
|
|
20138
|
+
} catch {
|
|
20139
|
+
return;
|
|
20140
|
+
}
|
|
20141
|
+
if (visited.has(real))
|
|
20142
|
+
return;
|
|
20143
|
+
if (!isContained(real, rootReal))
|
|
20144
|
+
return;
|
|
20145
|
+
visited.add(real);
|
|
19278
20146
|
let stats;
|
|
19279
20147
|
try {
|
|
19280
|
-
stats = await (0,
|
|
20148
|
+
stats = await (0, import_promises11.stat)(dirOrFile);
|
|
19281
20149
|
} catch {
|
|
19282
20150
|
return;
|
|
19283
20151
|
}
|
|
19284
20152
|
if (stats.isFile()) {
|
|
19285
|
-
if (SCANNABLE_EXTENSIONS2.has((0,
|
|
20153
|
+
if (SCANNABLE_EXTENSIONS2.has((0, import_node_path23.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
|
|
19286
20154
|
files.push(dirOrFile);
|
|
19287
20155
|
}
|
|
19288
20156
|
return;
|
|
@@ -19290,32 +20158,41 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
19290
20158
|
if (stats.isDirectory()) {
|
|
19291
20159
|
let entries;
|
|
19292
20160
|
try {
|
|
19293
|
-
entries = await (0,
|
|
20161
|
+
entries = await (0, import_promises11.readdir)(dirOrFile);
|
|
19294
20162
|
} catch {
|
|
19295
20163
|
return;
|
|
19296
20164
|
}
|
|
19297
20165
|
for (const entry of entries) {
|
|
19298
20166
|
if (SKIP_DIRS.has(entry))
|
|
19299
20167
|
continue;
|
|
19300
|
-
const fullPath = (0,
|
|
20168
|
+
const fullPath = (0, import_node_path23.join)(dirOrFile, entry);
|
|
19301
20169
|
await walk(fullPath);
|
|
19302
20170
|
}
|
|
19303
20171
|
}
|
|
19304
20172
|
}
|
|
19305
20173
|
try {
|
|
19306
|
-
await walk(installPath);
|
|
20174
|
+
await walk((0, import_node_path23.resolve)(installPath));
|
|
19307
20175
|
} catch (e) {
|
|
19308
20176
|
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
19309
20177
|
}
|
|
19310
20178
|
return files;
|
|
19311
20179
|
}
|
|
19312
20180
|
async function scanPlugin(plugin, options = {}) {
|
|
19313
|
-
const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger } = options;
|
|
20181
|
+
const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger, skillVerdictCachePath, skillPendingPath, loggingConfig, skillVerdictTtlMs, uploadEnabled = true, deferUnknownSkills = false, agentRuntime } = options;
|
|
19314
20182
|
const result = { plugin, findings: [] };
|
|
19315
|
-
const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger
|
|
20183
|
+
const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger, {
|
|
20184
|
+
verdictCachePath: skillVerdictCachePath,
|
|
20185
|
+
pendingPath: skillPendingPath,
|
|
20186
|
+
loggingConfig,
|
|
20187
|
+
verdictTtlMs: skillVerdictTtlMs,
|
|
20188
|
+
uploadEnabled,
|
|
20189
|
+
deferUnknownSkills,
|
|
20190
|
+
agentRuntime
|
|
20191
|
+
}) : Promise.resolve(false);
|
|
19316
20192
|
const files = await walkPluginFiles(plugin.installPath, logger);
|
|
19317
20193
|
if (files.length === 0) {
|
|
19318
|
-
await skillCheckPromise
|
|
20194
|
+
if (await skillCheckPromise)
|
|
20195
|
+
result.deferCache = true;
|
|
19319
20196
|
return result;
|
|
19320
20197
|
}
|
|
19321
20198
|
const allUrls = [];
|
|
@@ -19331,7 +20208,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19331
20208
|
}
|
|
19332
20209
|
if (amsiClient) {
|
|
19333
20210
|
try {
|
|
19334
|
-
const scanName = `${plugin.key}/${(0,
|
|
20211
|
+
const scanName = `${plugin.key}/${(0, import_node_path23.relative)(plugin.installPath, filePath)}`;
|
|
19335
20212
|
const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
|
|
19336
20213
|
if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
|
|
19337
20214
|
result.findings.push({
|
|
@@ -19339,7 +20216,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19339
20216
|
title: `AMSI detection (result=${amsiResult.amsiResult})`,
|
|
19340
20217
|
severity: "critical",
|
|
19341
20218
|
artifact: content.slice(0, 200),
|
|
19342
|
-
sourceFile: (0,
|
|
20219
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
|
|
19343
20220
|
});
|
|
19344
20221
|
}
|
|
19345
20222
|
} catch {
|
|
@@ -19392,7 +20269,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19392
20269
|
title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
|
|
19393
20270
|
severity: "critical",
|
|
19394
20271
|
artifact: fr.sha256,
|
|
19395
|
-
sourceFile: (0,
|
|
20272
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
|
|
19396
20273
|
});
|
|
19397
20274
|
}
|
|
19398
20275
|
}
|
|
@@ -19400,46 +20277,109 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19400
20277
|
} catch {
|
|
19401
20278
|
}
|
|
19402
20279
|
})() : Promise.resolve();
|
|
19403
|
-
await Promise.all([
|
|
20280
|
+
const [, , skillDeferCache] = await Promise.all([
|
|
20281
|
+
urlCheckPromise,
|
|
20282
|
+
fileCheckPromise,
|
|
20283
|
+
skillCheckPromise
|
|
20284
|
+
]);
|
|
20285
|
+
if (skillDeferCache)
|
|
20286
|
+
result.deferCache = true;
|
|
19404
20287
|
return result;
|
|
19405
20288
|
}
|
|
19406
|
-
|
|
20289
|
+
function pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict) {
|
|
20290
|
+
const risk = (verdict.verdict ?? "").toUpperCase();
|
|
20291
|
+
if (risk !== "HIGH" && risk !== "CRITICAL")
|
|
20292
|
+
return;
|
|
20293
|
+
const severity = risk === "CRITICAL" ? "critical" : "warning";
|
|
20294
|
+
findings.push({
|
|
20295
|
+
threatId: "SKILL_CHECK",
|
|
20296
|
+
title: verdict.summary?.trim() || (risk === "CRITICAL" ? "Malicious skill detected" : "Suspicious skill detected"),
|
|
20297
|
+
severity,
|
|
20298
|
+
artifact: skillId.slice(0, 16),
|
|
20299
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, folder) || "."
|
|
20300
|
+
});
|
|
20301
|
+
}
|
|
20302
|
+
async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
19407
20303
|
try {
|
|
19408
|
-
const skills = await computeSkillIdsForRoot(plugin.installPath);
|
|
20304
|
+
const skills = await computeSkillIdsForRoot(plugin.installPath, logger);
|
|
19409
20305
|
if (skills.length === 0)
|
|
19410
|
-
return;
|
|
19411
|
-
const
|
|
20306
|
+
return false;
|
|
20307
|
+
const verdictCache = await loadSkillVerdictCache(paths.verdictCachePath, paths.verdictTtlMs, logger);
|
|
20308
|
+
const uncached = [];
|
|
20309
|
+
for (const skill of skills) {
|
|
20310
|
+
const cached = getVerdict(verdictCache, skill.skillId, paths.verdictTtlMs);
|
|
20311
|
+
if (cached) {
|
|
20312
|
+
pushSkillFindingIfRisky(findings, plugin, skill.skillId, skill.folder, cached);
|
|
20313
|
+
} else {
|
|
20314
|
+
uncached.push(skill);
|
|
20315
|
+
}
|
|
20316
|
+
}
|
|
20317
|
+
if (uncached.length === 0)
|
|
20318
|
+
return false;
|
|
19412
20319
|
const client = new SkillCheckClient(void 0, logger);
|
|
19413
|
-
const verdicts = await client.checkSkills(
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
20320
|
+
const verdicts = await client.checkSkills(uncached.map((s) => s.skillId));
|
|
20321
|
+
const marker = await loadPendingMarker(paths.pendingPath, logger);
|
|
20322
|
+
let cacheModified = false;
|
|
20323
|
+
let markerModified = false;
|
|
20324
|
+
let lookupFailed = false;
|
|
20325
|
+
let graceDeferred = false;
|
|
20326
|
+
for (const { folder, skillId } of uncached) {
|
|
20327
|
+
if (!verdicts.has(skillId)) {
|
|
20328
|
+
lookupFailed = true;
|
|
19417
20329
|
continue;
|
|
19418
|
-
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
|
|
20330
|
+
}
|
|
20331
|
+
const verdict = verdicts.get(skillId) ?? null;
|
|
20332
|
+
if (verdict) {
|
|
20333
|
+
putVerdict(verdictCache, skillId, {
|
|
20334
|
+
verdict: verdict.verdict,
|
|
20335
|
+
summary: verdict.summary,
|
|
20336
|
+
skillName: (0, import_node_path23.basename)(folder),
|
|
20337
|
+
sources: [{ agentRuntime: paths.agentRuntime, containerKey: plugin.key }]
|
|
20338
|
+
});
|
|
20339
|
+
cacheModified = true;
|
|
20340
|
+
pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict);
|
|
20341
|
+
if (isPending(marker, skillId)) {
|
|
20342
|
+
removePending(marker, skillId);
|
|
20343
|
+
markerModified = true;
|
|
20344
|
+
}
|
|
20345
|
+
} else if (!isPending(marker, skillId)) {
|
|
20346
|
+
if (paths.uploadEnabled === false) {
|
|
20347
|
+
if (paths.deferUnknownSkills)
|
|
20348
|
+
graceDeferred = true;
|
|
20349
|
+
} else {
|
|
20350
|
+
addPending(marker, skillId, folder, {
|
|
20351
|
+
agentRuntime: paths.agentRuntime,
|
|
20352
|
+
containerKey: plugin.key
|
|
20353
|
+
});
|
|
20354
|
+
markerModified = true;
|
|
20355
|
+
if (paths.loggingConfig) {
|
|
20356
|
+
logSkillQueued(paths.loggingConfig, skillId, plugin.key, folder).catch(() => {
|
|
20357
|
+
});
|
|
20358
|
+
}
|
|
20359
|
+
}
|
|
20360
|
+
}
|
|
19430
20361
|
}
|
|
20362
|
+
if (cacheModified)
|
|
20363
|
+
await saveSkillVerdictCache(verdictCache, paths.verdictCachePath, logger);
|
|
20364
|
+
if (markerModified)
|
|
20365
|
+
await savePendingMarker(marker, paths.pendingPath, logger);
|
|
20366
|
+
const anyStillPending = uncached.some(({ skillId }) => isPending(marker, skillId));
|
|
20367
|
+
return markerModified || anyStillPending || lookupFailed || graceDeferred;
|
|
19431
20368
|
} catch (e) {
|
|
19432
20369
|
logger.debug("Skill check failed", { error: String(e) });
|
|
20370
|
+
return false;
|
|
19433
20371
|
}
|
|
19434
20372
|
}
|
|
19435
20373
|
|
|
19436
20374
|
// ../core/dist/session-start.js
|
|
19437
20375
|
var import_node_child_process2 = require("node:child_process");
|
|
19438
20376
|
var import_node_fs4 = require("node:fs");
|
|
20377
|
+
var import_node_path25 = require("node:path");
|
|
19439
20378
|
init_config();
|
|
19440
20379
|
init_file_utils();
|
|
19441
20380
|
|
|
19442
20381
|
// ../core/dist/session-start-scan.js
|
|
20382
|
+
var import_node_path24 = require("node:path");
|
|
19443
20383
|
init_config();
|
|
19444
20384
|
init_types2();
|
|
19445
20385
|
function fromCachedFinding(finding) {
|
|
@@ -19522,6 +20462,9 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19522
20462
|
deniedByException: 0,
|
|
19523
20463
|
findingsCount: 0
|
|
19524
20464
|
};
|
|
20465
|
+
const skillPendingPath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_pending.json") : void 0;
|
|
20466
|
+
const skillVerdictCachePath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_verdict_cache.json") : void 0;
|
|
20467
|
+
const cacheTtlMs = skillCacheTtlMs(config2);
|
|
19525
20468
|
for (const plugin of plugins) {
|
|
19526
20469
|
const denyMatch = findPluginDenyException(exceptions, plugin.key);
|
|
19527
20470
|
if (denyMatch) {
|
|
@@ -19546,7 +20489,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19546
20489
|
stats.allowedByException += 1;
|
|
19547
20490
|
continue;
|
|
19548
20491
|
}
|
|
19549
|
-
const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
|
|
20492
|
+
const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated, cacheTtlMs);
|
|
19550
20493
|
if (cached && cached.findings.length === 0) {
|
|
19551
20494
|
stats.cacheHitsClean += 1;
|
|
19552
20495
|
continue;
|
|
@@ -19565,11 +20508,23 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19565
20508
|
const result = await scanPlugin(plugin, {
|
|
19566
20509
|
checkUrls: context.checkUrls ?? true,
|
|
19567
20510
|
checkFileHashes: context.checkFileHashes ?? true,
|
|
20511
|
+
checkSkills: config2.skill_check.enabled,
|
|
20512
|
+
uploadEnabled: context.uploadUnknownSkills ?? config2.skill_check.upload_enabled,
|
|
20513
|
+
deferUnknownSkills: context.deferUnknownSkills,
|
|
19568
20514
|
amsiClient,
|
|
19569
|
-
logger
|
|
20515
|
+
logger,
|
|
20516
|
+
skillPendingPath,
|
|
20517
|
+
skillVerdictCachePath,
|
|
20518
|
+
skillVerdictTtlMs: cacheTtlMs,
|
|
20519
|
+
loggingConfig: config2.logging,
|
|
20520
|
+
agentRuntime: context.agentRuntime
|
|
19570
20521
|
});
|
|
19571
|
-
|
|
19572
|
-
|
|
20522
|
+
if (result.deferCache) {
|
|
20523
|
+
logger.debug("Plugin cache deferred (skill verdict pending)", { plugin: plugin.key });
|
|
20524
|
+
} else {
|
|
20525
|
+
storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
|
|
20526
|
+
cacheModified = true;
|
|
20527
|
+
}
|
|
19573
20528
|
if (result.findings.length > 0) {
|
|
19574
20529
|
stats.findingsCount += result.findings.length;
|
|
19575
20530
|
resultsWithFindings.push(result);
|
|
@@ -19596,6 +20551,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19596
20551
|
init_types2();
|
|
19597
20552
|
|
|
19598
20553
|
// ../core/dist/version-check.js
|
|
20554
|
+
init_config();
|
|
19599
20555
|
init_types2();
|
|
19600
20556
|
var DEFAULT_TIMEOUT_MS3 = 5e3;
|
|
19601
20557
|
function isNewerVersion(current, latest) {
|
|
@@ -19622,11 +20578,20 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
|
|
|
19622
20578
|
logger.debug("Skipping version check: missing installation id");
|
|
19623
20579
|
return null;
|
|
19624
20580
|
}
|
|
20581
|
+
let config2 = context.config;
|
|
20582
|
+
if (!config2) {
|
|
20583
|
+
try {
|
|
20584
|
+
config2 = await loadConfig(context.configPath, logger);
|
|
20585
|
+
} catch (err) {
|
|
20586
|
+
logger.debug(`Version check config load failed: ${err}`);
|
|
20587
|
+
}
|
|
20588
|
+
}
|
|
19625
20589
|
const envelope = buildSageProxyEnvelope({
|
|
19626
20590
|
iid: context.iid,
|
|
19627
20591
|
versionApp: currentVersion,
|
|
19628
20592
|
agentRuntime: context.agentRuntime,
|
|
19629
|
-
agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown"
|
|
20593
|
+
agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown",
|
|
20594
|
+
config: config2
|
|
19630
20595
|
});
|
|
19631
20596
|
const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
|
|
19632
20597
|
const enriched = mergeExtendedInfo(envelope, extendedInfo);
|
|
@@ -19666,6 +20631,8 @@ async function runSessionStart(ctx) {
|
|
|
19666
20631
|
const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
|
|
19667
20632
|
pruneOrphanedTmpFiles(sageDirPath).catch(() => {
|
|
19668
20633
|
});
|
|
20634
|
+
deployConfigDefaults(sageDirPath, logger).catch(() => {
|
|
20635
|
+
});
|
|
19669
20636
|
maybeSpawnModelDownloadWorker({
|
|
19670
20637
|
sageDirPath,
|
|
19671
20638
|
workerPath: ctx.modelDownloadWorkerPath,
|
|
@@ -19676,6 +20643,13 @@ async function runSessionStart(ctx) {
|
|
|
19676
20643
|
logger
|
|
19677
20644
|
}).catch(() => {
|
|
19678
20645
|
});
|
|
20646
|
+
const rollout = await resolveSkillUploadRollout({
|
|
20647
|
+
sageDirPath,
|
|
20648
|
+
configPath: ctx.configPath,
|
|
20649
|
+
version: ctx.version,
|
|
20650
|
+
logger,
|
|
20651
|
+
commitNotice: ctx.commitUploadNotice ?? true
|
|
20652
|
+
});
|
|
19679
20653
|
const iidPromise = getInstallationId(sageDirPath).catch(() => void 0);
|
|
19680
20654
|
const [scanResults, versionCheck] = await Promise.all([
|
|
19681
20655
|
runSessionStartScan({
|
|
@@ -19687,7 +20661,11 @@ async function runSessionStart(ctx) {
|
|
|
19687
20661
|
configPath: ctx.configPath,
|
|
19688
20662
|
scanCachePath: ctx.scanCachePath,
|
|
19689
20663
|
checkUrls: ctx.checkUrls,
|
|
19690
|
-
checkFileHashes: ctx.checkFileHashes
|
|
20664
|
+
checkFileHashes: ctx.checkFileHashes,
|
|
20665
|
+
sageDirPath,
|
|
20666
|
+
agentRuntime: ctx.agentRuntime,
|
|
20667
|
+
uploadUnknownSkills: rollout.uploadActive,
|
|
20668
|
+
deferUnknownSkills: rollout.deferUnknownSkills
|
|
19691
20669
|
}),
|
|
19692
20670
|
iidPromise.then((iid) => {
|
|
19693
20671
|
if (!iid) {
|
|
@@ -19696,11 +20674,25 @@ async function runSessionStart(ctx) {
|
|
|
19696
20674
|
return checkForUpdate(ctx.version, logger, void 0, {
|
|
19697
20675
|
agentRuntime: ctx.agentRuntime,
|
|
19698
20676
|
agentRuntimeVersion: ctx.agentRuntimeVersion,
|
|
19699
|
-
iid
|
|
20677
|
+
iid,
|
|
20678
|
+
configPath: ctx.configPath
|
|
19700
20679
|
});
|
|
19701
20680
|
}).catch(() => null)
|
|
19702
20681
|
]);
|
|
19703
|
-
|
|
20682
|
+
maybeSpawnSkillUploadWorker({
|
|
20683
|
+
sageDirPath,
|
|
20684
|
+
workerPath: ctx.skillUploadWorkerPath,
|
|
20685
|
+
configPath: ctx.configPath,
|
|
20686
|
+
agentRuntime: ctx.agentRuntime,
|
|
20687
|
+
uploadActive: rollout.uploadActive,
|
|
20688
|
+
logger
|
|
20689
|
+
}).catch(() => {
|
|
20690
|
+
});
|
|
20691
|
+
return {
|
|
20692
|
+
scanResults,
|
|
20693
|
+
versionCheck,
|
|
20694
|
+
notices: rollout.showNotice ? [SKILL_UPLOAD_NOTICE.id] : []
|
|
20695
|
+
};
|
|
19704
20696
|
}
|
|
19705
20697
|
async function maybeSpawnModelDownloadWorker(args) {
|
|
19706
20698
|
if (!args.workerPath) {
|
|
@@ -19746,6 +20738,7 @@ async function maybeSpawnModelDownloadWorker(args) {
|
|
|
19746
20738
|
const spawned = spawnModelDownloadWorker({
|
|
19747
20739
|
sageDirPath: args.sageDirPath,
|
|
19748
20740
|
workerPath: args.workerPath,
|
|
20741
|
+
configPath: args.configPath,
|
|
19749
20742
|
agentRuntime: args.agentRuntime,
|
|
19750
20743
|
agentRuntimeVersion: args.agentRuntimeVersion,
|
|
19751
20744
|
versionApp: args.versionApp,
|
|
@@ -19757,6 +20750,9 @@ async function maybeSpawnModelDownloadWorker(args) {
|
|
|
19757
20750
|
missingModels
|
|
19758
20751
|
});
|
|
19759
20752
|
}
|
|
20753
|
+
function bunRuntimeEnv() {
|
|
20754
|
+
return process.versions.bun ? { BUN_BE_BUN: "1" } : {};
|
|
20755
|
+
}
|
|
19760
20756
|
function spawnModelDownloadWorker(args) {
|
|
19761
20757
|
const logger = args.logger ?? nullLogger;
|
|
19762
20758
|
try {
|
|
@@ -19765,7 +20761,9 @@ function spawnModelDownloadWorker(args) {
|
|
|
19765
20761
|
stdio: "ignore",
|
|
19766
20762
|
env: {
|
|
19767
20763
|
...process.env,
|
|
20764
|
+
...bunRuntimeEnv(),
|
|
19768
20765
|
SAGE_DIR: args.sageDirPath,
|
|
20766
|
+
SAGE_CONFIG_PATH: args.configPath ?? "",
|
|
19769
20767
|
SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown"),
|
|
19770
20768
|
SAGE_AGENT_RUNTIME_VERSION: args.agentRuntimeVersion ?? "",
|
|
19771
20769
|
SAGE_VERSION_APP: args.versionApp ?? "",
|
|
@@ -19779,9 +20777,83 @@ function spawnModelDownloadWorker(args) {
|
|
|
19779
20777
|
return false;
|
|
19780
20778
|
}
|
|
19781
20779
|
}
|
|
20780
|
+
async function maybeSpawnSkillUploadWorker(args) {
|
|
20781
|
+
if (!args.uploadActive) {
|
|
20782
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20783
|
+
result: "skipped",
|
|
20784
|
+
skippedReason: "upload_inactive"
|
|
20785
|
+
});
|
|
20786
|
+
return;
|
|
20787
|
+
}
|
|
20788
|
+
try {
|
|
20789
|
+
const config2 = await loadConfig(args.configPath, args.logger);
|
|
20790
|
+
if (!config2.skill_check.enabled) {
|
|
20791
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20792
|
+
result: "skipped",
|
|
20793
|
+
skippedReason: "skill_check_disabled"
|
|
20794
|
+
});
|
|
20795
|
+
return;
|
|
20796
|
+
}
|
|
20797
|
+
} catch {
|
|
20798
|
+
}
|
|
20799
|
+
if (!args.workerPath) {
|
|
20800
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20801
|
+
result: "skipped",
|
|
20802
|
+
skippedReason: "missing_worker_path"
|
|
20803
|
+
});
|
|
20804
|
+
return;
|
|
20805
|
+
}
|
|
20806
|
+
if (!(0, import_node_fs4.existsSync)(args.workerPath)) {
|
|
20807
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20808
|
+
result: "skipped",
|
|
20809
|
+
skippedReason: "worker_script_not_found",
|
|
20810
|
+
workerPath: args.workerPath
|
|
20811
|
+
});
|
|
20812
|
+
return;
|
|
20813
|
+
}
|
|
20814
|
+
const marker = await loadPendingMarker((0, import_node_path25.join)(args.sageDirPath, "skill_pending.json"), args.logger);
|
|
20815
|
+
const pendingCount = listPending(marker).length;
|
|
20816
|
+
if (pendingCount === 0) {
|
|
20817
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20818
|
+
result: "skipped",
|
|
20819
|
+
skippedReason: "no_pending_skills"
|
|
20820
|
+
});
|
|
20821
|
+
return;
|
|
20822
|
+
}
|
|
20823
|
+
const spawned = spawnSkillUploadWorker({
|
|
20824
|
+
sageDirPath: args.sageDirPath,
|
|
20825
|
+
workerPath: args.workerPath,
|
|
20826
|
+
agentRuntime: args.agentRuntime,
|
|
20827
|
+
logger: args.logger
|
|
20828
|
+
});
|
|
20829
|
+
args.logger.debug("Skill upload worker spawn completed", {
|
|
20830
|
+
result: spawned ? "spawned" : "failed",
|
|
20831
|
+
pendingCount
|
|
20832
|
+
});
|
|
20833
|
+
}
|
|
20834
|
+
function spawnSkillUploadWorker(args) {
|
|
20835
|
+
const logger = args.logger ?? nullLogger;
|
|
20836
|
+
try {
|
|
20837
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [args.workerPath], {
|
|
20838
|
+
detached: true,
|
|
20839
|
+
stdio: "ignore",
|
|
20840
|
+
env: {
|
|
20841
|
+
...process.env,
|
|
20842
|
+
...bunRuntimeEnv(),
|
|
20843
|
+
SAGE_DIR: args.sageDirPath,
|
|
20844
|
+
SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown")
|
|
20845
|
+
}
|
|
20846
|
+
});
|
|
20847
|
+
child.unref();
|
|
20848
|
+
return true;
|
|
20849
|
+
} catch (err) {
|
|
20850
|
+
logger.warn("Failed to spawn skill upload worker", { error: String(err) });
|
|
20851
|
+
return false;
|
|
20852
|
+
}
|
|
20853
|
+
}
|
|
19782
20854
|
|
|
19783
20855
|
// ../core/dist/scan-handler.js
|
|
19784
|
-
async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion) {
|
|
20856
|
+
async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans = true) {
|
|
19785
20857
|
logger.debug(`${branding2.name} plugin scan started (${context})`, {
|
|
19786
20858
|
agentRuntime,
|
|
19787
20859
|
pluginsCount: plugins.length,
|
|
@@ -19796,8 +20868,20 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
|
|
|
19796
20868
|
logger,
|
|
19797
20869
|
agentRuntime,
|
|
19798
20870
|
agentRuntimeVersion,
|
|
19799
|
-
modelDownloadWorkerPath
|
|
20871
|
+
modelDownloadWorkerPath,
|
|
20872
|
+
skillUploadWorkerPath,
|
|
20873
|
+
commitUploadNotice
|
|
19800
20874
|
});
|
|
20875
|
+
if (result.notices && result.notices.length > 0) {
|
|
20876
|
+
logger.debug(`${branding2.name} session-start produced notices`, {
|
|
20877
|
+
context,
|
|
20878
|
+
noticeIds: result.notices,
|
|
20879
|
+
forwarded: Boolean(onNotices)
|
|
20880
|
+
});
|
|
20881
|
+
onNotices?.(result.notices);
|
|
20882
|
+
} else {
|
|
20883
|
+
logger.debug(`${branding2.name} session-start produced no notices`, { context });
|
|
20884
|
+
}
|
|
19801
20885
|
const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
|
|
19802
20886
|
const completionData = {
|
|
19803
20887
|
agentRuntime,
|
|
@@ -19811,10 +20895,13 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
|
|
|
19811
20895
|
} else {
|
|
19812
20896
|
logger.debug(`${branding2.name} plugin scan (${context}) complete`, completionData);
|
|
19813
20897
|
}
|
|
20898
|
+
if (findingsCount === 0 && !announceCleanScans) {
|
|
20899
|
+
return "";
|
|
20900
|
+
}
|
|
19814
20901
|
return formatSessionStartMessage(version, result, branding2, style);
|
|
19815
20902
|
}
|
|
19816
20903
|
function createScanHandler(options) {
|
|
19817
|
-
const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
|
|
20904
|
+
const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, onNotices, modelDownloadWorkerPath, skillUploadWorkerPath, style = "verbose", commitUploadNotice, announceCleanScans = true } = options;
|
|
19818
20905
|
return async () => {
|
|
19819
20906
|
try {
|
|
19820
20907
|
let plugins = await discoverPlugins2();
|
|
@@ -19822,8 +20909,10 @@ function createScanHandler(options) {
|
|
|
19822
20909
|
if (plugins.length === 0) {
|
|
19823
20910
|
logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
|
|
19824
20911
|
}
|
|
19825
|
-
const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion);
|
|
19826
|
-
|
|
20912
|
+
const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans);
|
|
20913
|
+
if (msg !== "") {
|
|
20914
|
+
onResult?.(msg);
|
|
20915
|
+
}
|
|
19827
20916
|
} catch (e) {
|
|
19828
20917
|
logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
|
|
19829
20918
|
}
|
|
@@ -19861,19 +20950,19 @@ function canonicalizeToolName(map, rawName) {
|
|
|
19861
20950
|
init_types2();
|
|
19862
20951
|
|
|
19863
20952
|
// src/bundled-dirs.ts
|
|
19864
|
-
var
|
|
19865
|
-
var packageRoot = (0,
|
|
20953
|
+
var import_node_path26 = require("node:path");
|
|
20954
|
+
var packageRoot = (0, import_node_path26.resolve)(__dirname, "..");
|
|
19866
20955
|
function getBundledDataDirs() {
|
|
19867
20956
|
return {
|
|
19868
|
-
threatsDir: (0,
|
|
19869
|
-
trustedDomainsDir: (0,
|
|
20957
|
+
threatsDir: (0, import_node_path26.join)(packageRoot, "resources", "threats"),
|
|
20958
|
+
trustedDomainsDir: (0, import_node_path26.join)(packageRoot, "resources", "trusted-domains")
|
|
19870
20959
|
};
|
|
19871
20960
|
}
|
|
19872
20961
|
var cachedVersion;
|
|
19873
20962
|
function getSageVersion() {
|
|
19874
20963
|
if (cachedVersion) return cachedVersion;
|
|
19875
20964
|
try {
|
|
19876
|
-
const pkg = JSON.parse(getFileContentSync((0,
|
|
20965
|
+
const pkg = JSON.parse(getFileContentSync((0, import_node_path26.join)(packageRoot, "package.json"), "utf-8"));
|
|
19877
20966
|
cachedVersion = pkg.version ?? "0.0.0";
|
|
19878
20967
|
} catch {
|
|
19879
20968
|
cachedVersion = "0.0.0";
|
|
@@ -19882,37 +20971,41 @@ function getSageVersion() {
|
|
|
19882
20971
|
}
|
|
19883
20972
|
|
|
19884
20973
|
// src/startup-scan.ts
|
|
19885
|
-
var
|
|
20974
|
+
var import_node_path28 = require("node:path");
|
|
19886
20975
|
|
|
19887
20976
|
// src/plugin-discovery.ts
|
|
19888
|
-
var
|
|
20977
|
+
var import_promises12 = require("node:fs/promises");
|
|
19889
20978
|
var import_node_os4 = require("node:os");
|
|
19890
|
-
var
|
|
19891
|
-
var DEFAULT_EXTENSIONS_DIR = (0,
|
|
19892
|
-
|
|
20979
|
+
var import_node_path27 = require("node:path");
|
|
20980
|
+
var DEFAULT_EXTENSIONS_DIR = (0, import_node_path27.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
|
|
20981
|
+
var SKILL_ROOTS = [
|
|
20982
|
+
{ dir: (0, import_node_path27.join)(getHomeDir(), ".agents", "skills"), tag: "agents", scope: "personal" },
|
|
20983
|
+
{ dir: (0, import_node_path27.join)(getHomeDir(), ".openclaw", "skills"), tag: "openclaw", scope: "personal" }
|
|
20984
|
+
];
|
|
20985
|
+
async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding, skillRoots = SKILL_ROOTS) {
|
|
19893
20986
|
logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
|
|
19894
20987
|
path: extensionsDir
|
|
19895
20988
|
});
|
|
20989
|
+
const plugins = [];
|
|
19896
20990
|
let entries;
|
|
19897
20991
|
try {
|
|
19898
|
-
entries = await (0,
|
|
20992
|
+
entries = await (0, import_promises12.readdir)(extensionsDir);
|
|
19899
20993
|
} catch {
|
|
19900
20994
|
logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
|
|
19901
|
-
|
|
20995
|
+
entries = [];
|
|
19902
20996
|
}
|
|
19903
|
-
const plugins = [];
|
|
19904
20997
|
for (const entry of entries) {
|
|
19905
|
-
const extDir = (0,
|
|
20998
|
+
const extDir = (0, import_node_path27.join)(extensionsDir, entry);
|
|
19906
20999
|
let stats;
|
|
19907
21000
|
try {
|
|
19908
|
-
stats = await (0,
|
|
21001
|
+
stats = await (0, import_promises12.stat)(extDir);
|
|
19909
21002
|
if (!stats.isDirectory()) continue;
|
|
19910
21003
|
} catch {
|
|
19911
21004
|
continue;
|
|
19912
21005
|
}
|
|
19913
21006
|
let pkg;
|
|
19914
21007
|
try {
|
|
19915
|
-
const pkgPath = (0,
|
|
21008
|
+
const pkgPath = (0, import_node_path27.join)(extDir, "package.json");
|
|
19916
21009
|
const raw = await getFileContent(pkgPath);
|
|
19917
21010
|
pkg = JSON.parse(raw);
|
|
19918
21011
|
} catch {
|
|
@@ -19934,11 +21027,14 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
19934
21027
|
});
|
|
19935
21028
|
}
|
|
19936
21029
|
logger.debug(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
|
|
21030
|
+
const skills = await discoverLooseSkillsAcrossRoots(skillRoots);
|
|
21031
|
+
logger.debug(`${branding2.name} skill discovery: found ${skills.length} loose skill(s)`);
|
|
21032
|
+
plugins.push(...skills);
|
|
19937
21033
|
return plugins;
|
|
19938
21034
|
}
|
|
19939
21035
|
|
|
19940
21036
|
// src/startup-scan.ts
|
|
19941
|
-
function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult) {
|
|
21037
|
+
function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult, announceCleanScans = true) {
|
|
19942
21038
|
const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
|
|
19943
21039
|
const version = getSageVersion();
|
|
19944
21040
|
return createScanHandler({
|
|
@@ -19953,29 +21049,58 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
|
|
|
19953
21049
|
// No agentRuntimeVersion: the OpenClaw plugin API exposes no host version.
|
|
19954
21050
|
branding: branding2,
|
|
19955
21051
|
onResult,
|
|
19956
|
-
|
|
21052
|
+
// No onNotices: OpenClaw delivers the one-time notice from
|
|
21053
|
+
// `before_agent_start` (a different process/turn than this scan) via
|
|
21054
|
+
// takePendingNotices, so the scan-time callback would never fire.
|
|
21055
|
+
modelDownloadWorkerPath: (0, import_node_path28.resolve)(__dirname, "model-download-worker.cjs"),
|
|
21056
|
+
skillUploadWorkerPath: (0, import_node_path28.resolve)(__dirname, "skill-upload-worker.cjs"),
|
|
21057
|
+
// Don't commit the "shown" flag here — commit happens at delivery.
|
|
21058
|
+
commitUploadNotice: false,
|
|
21059
|
+
announceCleanScans
|
|
19957
21060
|
});
|
|
19958
21061
|
}
|
|
19959
|
-
function createStartupScanHandler(logger, branding2 = defaultBranding, onResult) {
|
|
19960
|
-
return createOpenClawScanHandler(logger, "startup", branding2, onResult);
|
|
19961
|
-
}
|
|
19962
|
-
function createSessionScanHandler(logger, branding2 = defaultBranding, onResult) {
|
|
19963
|
-
return createOpenClawScanHandler(logger, "session", branding2, onResult);
|
|
19964
|
-
}
|
|
19965
|
-
function createBeforeAgentStartHandler(
|
|
19966
|
-
return () => {
|
|
19967
|
-
const findings =
|
|
19968
|
-
if (!findings)
|
|
19969
|
-
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
]
|
|
19978
|
-
|
|
21062
|
+
function createStartupScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
|
|
21063
|
+
return createOpenClawScanHandler(logger, "startup", branding2, onResult, announceCleanScans);
|
|
21064
|
+
}
|
|
21065
|
+
function createSessionScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
|
|
21066
|
+
return createOpenClawScanHandler(logger, "session", branding2, onResult, announceCleanScans);
|
|
21067
|
+
}
|
|
21068
|
+
function createBeforeAgentStartHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
|
|
21069
|
+
return (notices) => {
|
|
21070
|
+
const findings = getSecurityFindings();
|
|
21071
|
+
if (!notices && !findings) {
|
|
21072
|
+
logger.debug(`${branding2.name}: before_agent_start - nothing pending to surface`);
|
|
21073
|
+
return void 0;
|
|
21074
|
+
}
|
|
21075
|
+
if (findings) clearFindings();
|
|
21076
|
+
logger.debug(`${branding2.name}: surfacing context via before_agent_start`, {
|
|
21077
|
+
hasNotices: Boolean(notices),
|
|
21078
|
+
hasFindings: Boolean(findings)
|
|
21079
|
+
});
|
|
21080
|
+
const blocks = [];
|
|
21081
|
+
if (notices) {
|
|
21082
|
+
blocks.push(
|
|
21083
|
+
[
|
|
21084
|
+
'<sage-notice source="sage-session-start">',
|
|
21085
|
+
notices,
|
|
21086
|
+
"",
|
|
21087
|
+
"Relay this notice to the user. It is informational, not a security finding.",
|
|
21088
|
+
"</sage-notice>"
|
|
21089
|
+
].join("\n")
|
|
21090
|
+
);
|
|
21091
|
+
}
|
|
21092
|
+
if (findings) {
|
|
21093
|
+
blocks.push(
|
|
21094
|
+
[
|
|
21095
|
+
'<security-alert source="sage-plugin-scan">',
|
|
21096
|
+
findings,
|
|
21097
|
+
"",
|
|
21098
|
+
"Inform the user about these security findings.",
|
|
21099
|
+
"</security-alert>"
|
|
21100
|
+
].join("\n")
|
|
21101
|
+
);
|
|
21102
|
+
}
|
|
21103
|
+
return { prependContext: blocks.join("\n\n") };
|
|
19979
21104
|
};
|
|
19980
21105
|
}
|
|
19981
21106
|
|
|
@@ -20062,6 +21187,17 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
|
|
|
20062
21187
|
});
|
|
20063
21188
|
try {
|
|
20064
21189
|
const { toolName, params } = event;
|
|
21190
|
+
if (captureEnabled()) {
|
|
21191
|
+
await captureHookInput(
|
|
21192
|
+
"PreToolUse",
|
|
21193
|
+
{ toolName, params, sessionKey: ctx?.sessionKey },
|
|
21194
|
+
{
|
|
21195
|
+
toolName: canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName),
|
|
21196
|
+
toolInput: normalizeToolInput(toolName, params)
|
|
21197
|
+
},
|
|
21198
|
+
{ filePrefix: "openclaw-" }
|
|
21199
|
+
);
|
|
21200
|
+
}
|
|
20065
21201
|
const artifacts = mapToolToArtifacts(toolName, params);
|
|
20066
21202
|
if (!artifacts || artifacts.length === 0) {
|
|
20067
21203
|
completeHook("skipped", { skippedReason: "no_artifacts" });
|
|
@@ -20102,6 +21238,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
|
|
|
20102
21238
|
}
|
|
20103
21239
|
const maxReasons = 3;
|
|
20104
21240
|
const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, maxReasons).join("; ") + (verdict.reasons.length > maxReasons ? `; ... and ${verdict.reasons.length - maxReasons} more` : "") : verdict.category;
|
|
21241
|
+
const rule = ruleLabel(verdict);
|
|
20105
21242
|
const intersected = artifacts.filter((a) => verdict.artifacts.includes(a.value));
|
|
20106
21243
|
const flaggedArtifacts = intersected.length > 0 ? intersected : artifacts;
|
|
20107
21244
|
completeHook("evaluated", {
|
|
@@ -20118,6 +21255,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
|
|
|
20118
21255
|
title: `${branding2.name}: ${verdict.category}`,
|
|
20119
21256
|
description: [
|
|
20120
21257
|
`Severity: ${verdict.severity}`,
|
|
21258
|
+
...rule ? [`Rule: ${rule}`] : [],
|
|
20121
21259
|
`Reason: ${reasons}`,
|
|
20122
21260
|
`Artifacts: ${summarizeArtifacts(artifacts)}`
|
|
20123
21261
|
].join("\n"),
|
|
@@ -20187,9 +21325,9 @@ ${notice}` : notice;
|
|
|
20187
21325
|
const onFindings = (msg) => {
|
|
20188
21326
|
pendingScanFindings = msg;
|
|
20189
21327
|
};
|
|
20190
|
-
const
|
|
21328
|
+
const getPendingSecurityFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
|
|
20191
21329
|
const beforeAgentStartHandler = createBeforeAgentStartHandler(
|
|
20192
|
-
|
|
21330
|
+
getPendingSecurityFindings,
|
|
20193
21331
|
() => {
|
|
20194
21332
|
pendingConfigurationWarnings = null;
|
|
20195
21333
|
pendingScanFindings = null;
|
|
@@ -20197,16 +21335,37 @@ ${notice}` : notice;
|
|
|
20197
21335
|
logger,
|
|
20198
21336
|
branding
|
|
20199
21337
|
);
|
|
21338
|
+
const resolveNoticeText = async () => {
|
|
21339
|
+
try {
|
|
21340
|
+
const ids = await takePendingNotices({ logger });
|
|
21341
|
+
const text = ids.map((id) => formatNoticeById(id, branding)).filter(Boolean).join("\n\n");
|
|
21342
|
+
logger.debug(`${branding.name}: notices resolved for delivery`, {
|
|
21343
|
+
noticeIds: ids,
|
|
21344
|
+
willShow: text.length > 0
|
|
21345
|
+
});
|
|
21346
|
+
return text.length > 0 ? text : null;
|
|
21347
|
+
} catch (e) {
|
|
21348
|
+
logger.warn(`${branding.name}: notice resolution failed`, { error: String(e) });
|
|
21349
|
+
return null;
|
|
21350
|
+
}
|
|
21351
|
+
};
|
|
20200
21352
|
api.on(
|
|
20201
21353
|
"before_tool_call",
|
|
20202
21354
|
createToolCallHandler(approvalStore, toolLogger, threatsDir, trustedDomainsDir, branding),
|
|
20203
21355
|
{ priority: 100 }
|
|
20204
21356
|
);
|
|
20205
|
-
api.on(
|
|
20206
|
-
|
|
21357
|
+
api.on(
|
|
21358
|
+
"gateway_start",
|
|
21359
|
+
createStartupScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
|
|
21360
|
+
);
|
|
21361
|
+
api.on(
|
|
21362
|
+
"session_start",
|
|
21363
|
+
createSessionScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
|
|
21364
|
+
);
|
|
20207
21365
|
api.on("before_agent_start", async () => {
|
|
20208
21366
|
await migrationCheckPromise;
|
|
20209
|
-
|
|
21367
|
+
const notices = await resolveNoticeText();
|
|
21368
|
+
return beforeAgentStartHandler(notices);
|
|
20210
21369
|
});
|
|
20211
21370
|
}
|
|
20212
21371
|
};
|