@gendigital/sage-openclaw 0.11.0 → 0.12.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 +1444 -468
- package/dist/index.cjs.map +4 -4
- package/dist/mcp-server.cjs +194 -88
- package/dist/mcp-server.cjs.map +4 -4
- package/dist/model-download-worker.cjs +136 -93
- package/dist/model-download-worker.cjs.map +3 -3
- package/dist/skill-upload-worker.cjs +5632 -0
- package/dist/skill-upload-worker.cjs.map +7 -0
- package/openclaw.plugin.json +6 -1
- package/package.json +8 -3
- package/resources/threats/dummy.yaml +22 -17
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";
|
|
@@ -4328,14 +4369,24 @@ var init_types2 = __esm({
|
|
|
4328
4369
|
AmsiCheckConfigSchema = external_exports.object({
|
|
4329
4370
|
enabled: external_exports.boolean().default(true)
|
|
4330
4371
|
});
|
|
4372
|
+
SkillCheckConfigSchema = external_exports.object({
|
|
4373
|
+
enabled: external_exports.boolean().default(true),
|
|
4374
|
+
cache_ttl_days: external_exports.number().min(0).default(1),
|
|
4375
|
+
/**
|
|
4376
|
+
* Upload unknown skills (never seen by the analyzer) for deep content
|
|
4377
|
+
* analysis. When false, the scan still looks skills up by content hash and
|
|
4378
|
+
* still flags known-risky ones — but no skill content ever leaves the
|
|
4379
|
+
* machine and the upload worker never runs (lookup-only mode).
|
|
4380
|
+
*/
|
|
4381
|
+
upload_enabled: external_exports.boolean().default(true)
|
|
4382
|
+
});
|
|
4331
4383
|
DEFAULT_PI_HIGH_RISK_THRESHOLD = 0.99;
|
|
4332
4384
|
DEFAULT_PI_MEDIUM_RISK_THRESHOLD = 0.5;
|
|
4385
|
+
DEFAULT_PI_TELEMETRY_THRESHOLD = 0.95;
|
|
4333
4386
|
PiCheckConfigSchema = external_exports.object({
|
|
4334
4387
|
enabled: external_exports.boolean().default(false),
|
|
4335
4388
|
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)
|
|
4389
|
+
model_path: external_exports.string().optional()
|
|
4339
4390
|
});
|
|
4340
4391
|
ExceptionDecisionSchema = external_exports.enum(["allow", "deny"]);
|
|
4341
4392
|
ExceptionMatchSchema = external_exports.enum(["executable", "domain", "path", "plugin", "regex"]);
|
|
@@ -4357,6 +4408,7 @@ var init_types2 = __esm({
|
|
|
4357
4408
|
file_check: FileCheckConfigSchema.default({}),
|
|
4358
4409
|
package_check: PackageCheckConfigSchema.default({}),
|
|
4359
4410
|
amsi_check: AmsiCheckConfigSchema.default({}),
|
|
4411
|
+
skill_check: SkillCheckConfigSchema.default({}),
|
|
4360
4412
|
pi_check: PiCheckConfigSchema.default({}),
|
|
4361
4413
|
heuristics_enabled: external_exports.boolean().default(true),
|
|
4362
4414
|
cache: CacheConfigSchema.default({}),
|
|
@@ -4365,6 +4417,7 @@ var init_types2 = __esm({
|
|
|
4365
4417
|
operational_logging: OperationalLoggingConfigSchema.default({}),
|
|
4366
4418
|
sensitivity: SensitivitySchema.default("balanced"),
|
|
4367
4419
|
disabled_threats: external_exports.array(external_exports.string()).default([]),
|
|
4420
|
+
announce_clean_scans: external_exports.boolean().default(true),
|
|
4368
4421
|
brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
|
|
4369
4422
|
community_iq: external_exports.boolean().default(true)
|
|
4370
4423
|
});
|
|
@@ -4383,13 +4436,27 @@ var init_types2 = __esm({
|
|
|
4383
4436
|
var config_exports = {};
|
|
4384
4437
|
__export(config_exports, {
|
|
4385
4438
|
HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
|
|
4439
|
+
MS_PER_DAY: () => MS_PER_DAY,
|
|
4386
4440
|
SAGE_DIR: () => SAGE_DIR,
|
|
4387
4441
|
defaultConfigPath: () => defaultConfigPath,
|
|
4388
4442
|
getClaudeConfigDir: () => getClaudeConfigDir,
|
|
4443
|
+
isFreshTimestamp: () => isFreshTimestamp,
|
|
4389
4444
|
loadConfig: () => loadConfig,
|
|
4390
4445
|
loadConfigSync: () => loadConfigSync,
|
|
4391
|
-
|
|
4446
|
+
readExplicitSkillUploadEnabled: () => readExplicitSkillUploadEnabled,
|
|
4447
|
+
resolvePath: () => resolvePath,
|
|
4448
|
+
skillCacheTtlMs: () => skillCacheTtlMs
|
|
4392
4449
|
});
|
|
4450
|
+
function skillCacheTtlMs(config2) {
|
|
4451
|
+
return Math.max(1, config2.skill_check.cache_ttl_days) * MS_PER_DAY;
|
|
4452
|
+
}
|
|
4453
|
+
function isFreshTimestamp(ts, ttlMs, now = Date.now()) {
|
|
4454
|
+
if (Number.isNaN(ts))
|
|
4455
|
+
return false;
|
|
4456
|
+
if (ts - now > CLOCK_SKEW_TOLERANCE_MS)
|
|
4457
|
+
return false;
|
|
4458
|
+
return now - ts < ttlMs;
|
|
4459
|
+
}
|
|
4393
4460
|
function resolvedSageDir() {
|
|
4394
4461
|
return resolvePath(SAGE_DIR);
|
|
4395
4462
|
}
|
|
@@ -4464,7 +4531,7 @@ function sanitizeBrandKey(data, logger) {
|
|
|
4464
4531
|
if (typeof brandKey === "string" && brandKey.length >= 1 && brandKey.length <= 32 && BRAND_KEY_RE.test(brandKey)) {
|
|
4465
4532
|
return data;
|
|
4466
4533
|
}
|
|
4467
|
-
logger.warn(`Invalid brand_key in config
|
|
4534
|
+
logger.warn(`Invalid brand_key in config - ignoring`, { brand_key: brandKey });
|
|
4468
4535
|
const { brand_key: _, ...rest } = data;
|
|
4469
4536
|
return rest;
|
|
4470
4537
|
}
|
|
@@ -4516,6 +4583,21 @@ function parseConfig(raw, path, logger) {
|
|
|
4516
4583
|
return defaultConfig(logger);
|
|
4517
4584
|
}
|
|
4518
4585
|
}
|
|
4586
|
+
async function readExplicitSkillUploadEnabled(configPath, logger = nullLogger) {
|
|
4587
|
+
const path = configPath ? resolvePath(configPath) : defaultConfigPath();
|
|
4588
|
+
try {
|
|
4589
|
+
const data = JSON.parse(await getFileContent(path));
|
|
4590
|
+
const skillCheck = data.skill_check;
|
|
4591
|
+
if (skillCheck && typeof skillCheck === "object" && !Array.isArray(skillCheck) && "upload_enabled" in skillCheck) {
|
|
4592
|
+
const value = skillCheck.upload_enabled;
|
|
4593
|
+
if (typeof value === "boolean")
|
|
4594
|
+
return { present: true, value };
|
|
4595
|
+
logger.warn("Config skill_check.upload_enabled is not a boolean; ignoring", { value });
|
|
4596
|
+
}
|
|
4597
|
+
} catch {
|
|
4598
|
+
}
|
|
4599
|
+
return { present: false, value: false };
|
|
4600
|
+
}
|
|
4519
4601
|
async function loadConfig(configPath, logger = nullLogger) {
|
|
4520
4602
|
const path = configPath ? resolvePath(configPath) : defaultConfigPath();
|
|
4521
4603
|
try {
|
|
@@ -4532,7 +4614,7 @@ function loadConfigSync(configPath, logger = nullLogger) {
|
|
|
4532
4614
|
return defaultConfig(logger);
|
|
4533
4615
|
}
|
|
4534
4616
|
}
|
|
4535
|
-
var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, BRAND_KEY_RE;
|
|
4617
|
+
var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
|
|
4536
4618
|
var init_config = __esm({
|
|
4537
4619
|
"../core/dist/config.js"() {
|
|
4538
4620
|
"use strict";
|
|
@@ -4541,6 +4623,8 @@ var init_config = __esm({
|
|
|
4541
4623
|
init_types2();
|
|
4542
4624
|
SAGE_DIR = "~/.sage";
|
|
4543
4625
|
HOOK_TIMEOUT_SECONDS = 8;
|
|
4626
|
+
MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
4627
|
+
CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1e3;
|
|
4544
4628
|
BRAND_KEY_RE = /^[a-z0-9_-]+$/u;
|
|
4545
4629
|
}
|
|
4546
4630
|
});
|
|
@@ -6805,7 +6889,7 @@ async function ensurePiDeps(modelPath, logger = nullLogger) {
|
|
|
6805
6889
|
}
|
|
6806
6890
|
}
|
|
6807
6891
|
function runNpm(cwd, args, logger) {
|
|
6808
|
-
return new Promise((
|
|
6892
|
+
return new Promise((resolve10, reject) => {
|
|
6809
6893
|
const isWindows = process.platform === "win32";
|
|
6810
6894
|
const npmCmd = isWindows ? "npm.cmd" : "npm";
|
|
6811
6895
|
const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
|
|
@@ -6813,7 +6897,7 @@ function runNpm(cwd, args, logger) {
|
|
|
6813
6897
|
logger.warn(`npm install error: ${stderr || error.message}`);
|
|
6814
6898
|
reject(error);
|
|
6815
6899
|
} else {
|
|
6816
|
-
|
|
6900
|
+
resolve10();
|
|
6817
6901
|
}
|
|
6818
6902
|
});
|
|
6819
6903
|
child.unref();
|
|
@@ -14139,7 +14223,7 @@ function resolveBranding(brandKey, logger) {
|
|
|
14139
14223
|
return defaultBranding;
|
|
14140
14224
|
const entry = BRANDS[brandKey];
|
|
14141
14225
|
if (!entry) {
|
|
14142
|
-
logger?.warn(`Unknown brand_key "${brandKey}" in config
|
|
14226
|
+
logger?.warn(`Unknown brand_key "${brandKey}" in config - using default branding`);
|
|
14143
14227
|
return defaultBranding;
|
|
14144
14228
|
}
|
|
14145
14229
|
return { ...entry, brand_key: brandKey };
|
|
@@ -14239,88 +14323,226 @@ var ApprovalStore = class {
|
|
|
14239
14323
|
// ../core/dist/audit-log.js
|
|
14240
14324
|
var import_node_crypto3 = require("node:crypto");
|
|
14241
14325
|
init_config();
|
|
14326
|
+
|
|
14327
|
+
// ../core/dist/content-snapshot.js
|
|
14328
|
+
var import_node_os2 = require("node:os");
|
|
14329
|
+
var CONTENT_FIELD_LIMITS = Object.freeze({
|
|
14330
|
+
command: 512,
|
|
14331
|
+
url: 512,
|
|
14332
|
+
file_path: 512,
|
|
14333
|
+
package_name: 256,
|
|
14334
|
+
package_version: 128,
|
|
14335
|
+
package_registry: 128
|
|
14336
|
+
});
|
|
14337
|
+
function safeTruncate(value, maxLen) {
|
|
14338
|
+
if (maxLen <= 0)
|
|
14339
|
+
return "";
|
|
14340
|
+
if (value.length <= maxLen)
|
|
14341
|
+
return value;
|
|
14342
|
+
const cutIndex = maxLen;
|
|
14343
|
+
const codeUnit = value.charCodeAt(cutIndex - 1);
|
|
14344
|
+
if (codeUnit >= 55296 && codeUnit <= 56319) {
|
|
14345
|
+
return value.slice(0, cutIndex - 1);
|
|
14346
|
+
}
|
|
14347
|
+
return value.slice(0, cutIndex);
|
|
14348
|
+
}
|
|
14349
|
+
function scrubHomePath(value) {
|
|
14350
|
+
const home = (0, import_node_os2.homedir)();
|
|
14351
|
+
if (!home)
|
|
14352
|
+
return value;
|
|
14353
|
+
const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
14354
|
+
if (!normalizedHome)
|
|
14355
|
+
return value;
|
|
14356
|
+
const normalizedValue = value.replace(/\\/g, "/");
|
|
14357
|
+
if (normalizedValue === normalizedHome)
|
|
14358
|
+
return "~";
|
|
14359
|
+
if (normalizedValue.startsWith(`${normalizedHome}/`)) {
|
|
14360
|
+
return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
|
|
14361
|
+
}
|
|
14362
|
+
return value;
|
|
14363
|
+
}
|
|
14364
|
+
function asString(v) {
|
|
14365
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
14366
|
+
}
|
|
14367
|
+
function resolveFilePath(toolInput) {
|
|
14368
|
+
return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
|
|
14369
|
+
}
|
|
14370
|
+
function resolveWebFetchUrl(toolInput) {
|
|
14371
|
+
const single = asString(toolInput.url);
|
|
14372
|
+
if (single)
|
|
14373
|
+
return single;
|
|
14374
|
+
const urls = toolInput.urls;
|
|
14375
|
+
if (Array.isArray(urls)) {
|
|
14376
|
+
for (const u of urls) {
|
|
14377
|
+
if (typeof u === "string" && u.length > 0)
|
|
14378
|
+
return u;
|
|
14379
|
+
}
|
|
14380
|
+
}
|
|
14381
|
+
return void 0;
|
|
14382
|
+
}
|
|
14383
|
+
function extractFirstApplyPatchPath(patchText) {
|
|
14384
|
+
if (!patchText)
|
|
14385
|
+
return void 0;
|
|
14386
|
+
const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
|
|
14387
|
+
if (headerMatch?.[1]) {
|
|
14388
|
+
const trimmed = headerMatch[1].trim();
|
|
14389
|
+
if (trimmed)
|
|
14390
|
+
return trimmed;
|
|
14391
|
+
}
|
|
14392
|
+
const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
|
|
14393
|
+
if (renameMatch?.[1]) {
|
|
14394
|
+
const raw = renameMatch[1].trim();
|
|
14395
|
+
const arrow = raw.indexOf(" -> ");
|
|
14396
|
+
if (arrow !== -1) {
|
|
14397
|
+
const src = raw.slice(0, arrow).trim();
|
|
14398
|
+
if (src)
|
|
14399
|
+
return src;
|
|
14400
|
+
const dst = raw.slice(arrow + 4).trim();
|
|
14401
|
+
if (dst)
|
|
14402
|
+
return dst;
|
|
14403
|
+
} else if (raw) {
|
|
14404
|
+
return raw;
|
|
14405
|
+
}
|
|
14406
|
+
}
|
|
14407
|
+
return void 0;
|
|
14408
|
+
}
|
|
14409
|
+
function extractMcpContent(toolInput) {
|
|
14410
|
+
const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
|
|
14411
|
+
const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
|
|
14412
|
+
return {
|
|
14413
|
+
command: asString(candidate.command),
|
|
14414
|
+
url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
|
|
14415
|
+
file_path: resolveFilePath(candidate)
|
|
14416
|
+
};
|
|
14417
|
+
}
|
|
14418
|
+
function firstUrlArtifact(artifacts) {
|
|
14419
|
+
for (const a of artifacts) {
|
|
14420
|
+
if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
|
|
14421
|
+
return a.value;
|
|
14422
|
+
}
|
|
14423
|
+
}
|
|
14424
|
+
return void 0;
|
|
14425
|
+
}
|
|
14426
|
+
function applyFieldLimits(content) {
|
|
14427
|
+
for (const [key, value] of Object.entries(content)) {
|
|
14428
|
+
if (typeof value !== "string")
|
|
14429
|
+
continue;
|
|
14430
|
+
let sanitized = value;
|
|
14431
|
+
if (key === "file_path" || key === "command") {
|
|
14432
|
+
sanitized = scrubHomePath(sanitized);
|
|
14433
|
+
}
|
|
14434
|
+
const limit = CONTENT_FIELD_LIMITS[key];
|
|
14435
|
+
if (limit && sanitized.length > limit) {
|
|
14436
|
+
sanitized = safeTruncate(sanitized, limit);
|
|
14437
|
+
}
|
|
14438
|
+
content[key] = sanitized;
|
|
14439
|
+
}
|
|
14440
|
+
}
|
|
14441
|
+
function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
|
|
14442
|
+
const content = {};
|
|
14443
|
+
switch (toolType) {
|
|
14444
|
+
case "Bash": {
|
|
14445
|
+
const command = asString(toolInput.command);
|
|
14446
|
+
if (command)
|
|
14447
|
+
content.command = command;
|
|
14448
|
+
break;
|
|
14449
|
+
}
|
|
14450
|
+
case "WebFetch": {
|
|
14451
|
+
const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
|
|
14452
|
+
if (url)
|
|
14453
|
+
content.url = url;
|
|
14454
|
+
break;
|
|
14455
|
+
}
|
|
14456
|
+
case "Write":
|
|
14457
|
+
case "Edit":
|
|
14458
|
+
case "Read":
|
|
14459
|
+
case "Delete": {
|
|
14460
|
+
const filePath = resolveFilePath(toolInput);
|
|
14461
|
+
if (filePath)
|
|
14462
|
+
content.file_path = filePath;
|
|
14463
|
+
break;
|
|
14464
|
+
}
|
|
14465
|
+
case "ApplyPatch": {
|
|
14466
|
+
const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
|
|
14467
|
+
const filePath = extractFirstApplyPatchPath(patchText);
|
|
14468
|
+
if (filePath)
|
|
14469
|
+
content.file_path = filePath;
|
|
14470
|
+
break;
|
|
14471
|
+
}
|
|
14472
|
+
case "MCP": {
|
|
14473
|
+
const mcp = extractMcpContent(toolInput);
|
|
14474
|
+
if (mcp.command)
|
|
14475
|
+
content.command = mcp.command;
|
|
14476
|
+
if (mcp.url)
|
|
14477
|
+
content.url = mcp.url;
|
|
14478
|
+
if (mcp.file_path)
|
|
14479
|
+
content.file_path = mcp.file_path;
|
|
14480
|
+
break;
|
|
14481
|
+
}
|
|
14482
|
+
}
|
|
14483
|
+
if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
|
|
14484
|
+
const firstUrl = signals.url_checks[0]?.url;
|
|
14485
|
+
if (firstUrl)
|
|
14486
|
+
content.url = firstUrl;
|
|
14487
|
+
}
|
|
14488
|
+
if (signals.package_checks && signals.package_checks.length > 0) {
|
|
14489
|
+
const p = signals.package_checks[0];
|
|
14490
|
+
if (p?.package_name)
|
|
14491
|
+
content.package_name = p.package_name;
|
|
14492
|
+
if (p?.package_version)
|
|
14493
|
+
content.package_version = p.package_version;
|
|
14494
|
+
if (p?.package_registry)
|
|
14495
|
+
content.package_registry = p.package_registry;
|
|
14496
|
+
}
|
|
14497
|
+
applyFieldLimits(content);
|
|
14498
|
+
return content;
|
|
14499
|
+
}
|
|
14500
|
+
|
|
14501
|
+
// ../core/dist/audit-log.js
|
|
14242
14502
|
init_file_utils();
|
|
14243
14503
|
|
|
14244
14504
|
// ../core/dist/jsonl-log-writer.js
|
|
14245
|
-
var
|
|
14505
|
+
var import_promises2 = require("node:fs/promises");
|
|
14246
14506
|
var import_node_path4 = require("node:path");
|
|
14247
|
-
var import_promises2 = require("node:timers/promises");
|
|
14248
14507
|
init_config();
|
|
14508
|
+
init_file_utils();
|
|
14249
14509
|
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
14510
|
async function shouldRotate(filePath, maxBytes, maxFiles) {
|
|
14254
14511
|
if (maxBytes <= 0 || maxFiles <= 0)
|
|
14255
14512
|
return false;
|
|
14256
14513
|
try {
|
|
14257
|
-
const s = await (0,
|
|
14514
|
+
const s = await (0, import_promises2.stat)(filePath);
|
|
14258
14515
|
return s.size >= maxBytes;
|
|
14259
14516
|
} catch {
|
|
14260
14517
|
return false;
|
|
14261
14518
|
}
|
|
14262
14519
|
}
|
|
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
14520
|
async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
|
|
14294
14521
|
if (!await shouldRotate(filePath, maxBytes, maxFiles))
|
|
14295
14522
|
return;
|
|
14296
|
-
|
|
14297
|
-
if (!releaseLock)
|
|
14298
|
-
return;
|
|
14299
|
-
try {
|
|
14523
|
+
await withFileLock(filePath, async () => {
|
|
14300
14524
|
if (!await shouldRotate(filePath, maxBytes, maxFiles))
|
|
14301
14525
|
return;
|
|
14302
14526
|
try {
|
|
14303
|
-
await (0,
|
|
14527
|
+
await (0, import_promises2.unlink)(`${filePath}.${maxFiles}`);
|
|
14304
14528
|
} catch {
|
|
14305
14529
|
}
|
|
14306
14530
|
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
14307
14531
|
try {
|
|
14308
|
-
await (0,
|
|
14532
|
+
await (0, import_promises2.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
|
|
14309
14533
|
} catch {
|
|
14310
14534
|
}
|
|
14311
14535
|
}
|
|
14312
14536
|
try {
|
|
14313
|
-
await (0,
|
|
14537
|
+
await (0, import_promises2.rename)(filePath, `${filePath}.1`);
|
|
14314
14538
|
} catch {
|
|
14315
14539
|
}
|
|
14316
|
-
}
|
|
14317
|
-
await releaseLock();
|
|
14318
|
-
}
|
|
14540
|
+
});
|
|
14319
14541
|
}
|
|
14320
14542
|
async function appendJsonlEntryNow(path, config2, entry) {
|
|
14321
|
-
await (0,
|
|
14543
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
14322
14544
|
await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
|
|
14323
|
-
await (0,
|
|
14545
|
+
await (0, import_promises2.appendFile)(path, `${JSON.stringify(entry)}
|
|
14324
14546
|
`);
|
|
14325
14547
|
}
|
|
14326
14548
|
async function appendJsonlEntry(config2, entry) {
|
|
@@ -14338,6 +14560,7 @@ async function appendJsonlEntry(config2, entry) {
|
|
|
14338
14560
|
}
|
|
14339
14561
|
|
|
14340
14562
|
// ../core/dist/audit-log.js
|
|
14563
|
+
init_types2();
|
|
14341
14564
|
var MAX_SUMMARY_LEN = 200;
|
|
14342
14565
|
var AUDIT_LOG_SCHEMA_VERSION = 1;
|
|
14343
14566
|
function toolInputSummary(toolName, toolInput) {
|
|
@@ -14406,6 +14629,22 @@ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
|
|
|
14406
14629
|
} catch {
|
|
14407
14630
|
}
|
|
14408
14631
|
}
|
|
14632
|
+
async function logSkillQueued(config2, skillId, pluginKey, skillFolder) {
|
|
14633
|
+
if (!config2.enabled)
|
|
14634
|
+
return;
|
|
14635
|
+
try {
|
|
14636
|
+
await appendEntry(config2, {
|
|
14637
|
+
type: "skill_queued",
|
|
14638
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14639
|
+
skill_id: skillId,
|
|
14640
|
+
plugin_key: pluginKey,
|
|
14641
|
+
// Home-scrubbed folder path: aids forensics when the same skill id shows
|
|
14642
|
+
// up under multiple plugins. Omitted when the caller has no path.
|
|
14643
|
+
...skillFolder ? { skill_folder: scrubHomePath(skillFolder) } : {}
|
|
14644
|
+
});
|
|
14645
|
+
} catch {
|
|
14646
|
+
}
|
|
14647
|
+
}
|
|
14409
14648
|
|
|
14410
14649
|
// ../core/dist/cache.js
|
|
14411
14650
|
init_config();
|
|
@@ -14844,19 +15083,19 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
14844
15083
|
return this.available;
|
|
14845
15084
|
}
|
|
14846
15085
|
waitForLine(timeout) {
|
|
14847
|
-
return new Promise((
|
|
15086
|
+
return new Promise((resolve10, reject) => {
|
|
14848
15087
|
const timer = setTimeout(() => {
|
|
14849
15088
|
this.pendingResponse = null;
|
|
14850
15089
|
reject(new Error("timeout"));
|
|
14851
15090
|
}, timeout);
|
|
14852
|
-
this.pendingResponse = { resolve:
|
|
15091
|
+
this.pendingResponse = { resolve: resolve10, reject, timer };
|
|
14853
15092
|
});
|
|
14854
15093
|
}
|
|
14855
15094
|
async enqueueScan(operation) {
|
|
14856
15095
|
const previous = this.scanQueue;
|
|
14857
15096
|
let release;
|
|
14858
|
-
this.scanQueue = new Promise((
|
|
14859
|
-
release =
|
|
15097
|
+
this.scanQueue = new Promise((resolve10) => {
|
|
15098
|
+
release = resolve10;
|
|
14860
15099
|
});
|
|
14861
15100
|
await previous;
|
|
14862
15101
|
try {
|
|
@@ -14904,10 +15143,10 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
14904
15143
|
const line = this.stdoutBuffer.slice(0, idx).trim();
|
|
14905
15144
|
this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
|
|
14906
15145
|
if (this.pendingResponse) {
|
|
14907
|
-
const { resolve:
|
|
15146
|
+
const { resolve: resolve10, timer } = this.pendingResponse;
|
|
14908
15147
|
this.pendingResponse = null;
|
|
14909
15148
|
clearTimeout(timer);
|
|
14910
|
-
|
|
15149
|
+
resolve10(line);
|
|
14911
15150
|
}
|
|
14912
15151
|
idx = this.stdoutBuffer.indexOf("\n");
|
|
14913
15152
|
}
|
|
@@ -15006,7 +15245,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15006
15245
|
if (!this.available)
|
|
15007
15246
|
return null;
|
|
15008
15247
|
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
15009
|
-
return new Promise((
|
|
15248
|
+
return new Promise((resolve10) => {
|
|
15010
15249
|
try {
|
|
15011
15250
|
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
|
|
15012
15251
|
let stdout = "";
|
|
@@ -15016,7 +15255,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15016
15255
|
ps.kill();
|
|
15017
15256
|
} catch {
|
|
15018
15257
|
}
|
|
15019
|
-
|
|
15258
|
+
resolve10(null);
|
|
15020
15259
|
}, PS_TIMEOUT);
|
|
15021
15260
|
ps.stdout?.on("data", (chunk) => {
|
|
15022
15261
|
stdout += chunk.toString();
|
|
@@ -15029,7 +15268,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15029
15268
|
ps.on("error", (err) => {
|
|
15030
15269
|
clearTimeout(timer);
|
|
15031
15270
|
this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
|
|
15032
|
-
|
|
15271
|
+
resolve10(null);
|
|
15033
15272
|
});
|
|
15034
15273
|
ps.on("exit", () => {
|
|
15035
15274
|
clearTimeout(timer);
|
|
@@ -15042,11 +15281,11 @@ var WslPowershellAmsiBackend = class {
|
|
|
15042
15281
|
contentName
|
|
15043
15282
|
});
|
|
15044
15283
|
}
|
|
15045
|
-
|
|
15284
|
+
resolve10(null);
|
|
15046
15285
|
return;
|
|
15047
15286
|
}
|
|
15048
15287
|
this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
|
|
15049
|
-
|
|
15288
|
+
resolve10(interpretAmsiResult(amsiResult, content, contentName));
|
|
15050
15289
|
});
|
|
15051
15290
|
const req = JSON.stringify({ content: truncated, contentName });
|
|
15052
15291
|
ps.stdin?.write(`${req}
|
|
@@ -15054,7 +15293,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15054
15293
|
ps.stdin?.end();
|
|
15055
15294
|
} catch (e) {
|
|
15056
15295
|
this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
|
|
15057
|
-
|
|
15296
|
+
resolve10(null);
|
|
15058
15297
|
}
|
|
15059
15298
|
});
|
|
15060
15299
|
}
|
|
@@ -15200,7 +15439,7 @@ init_file_utils();
|
|
|
15200
15439
|
var import_meta = {};
|
|
15201
15440
|
function resolveVersion() {
|
|
15202
15441
|
if (true)
|
|
15203
|
-
return "0.
|
|
15442
|
+
return "0.12.0";
|
|
15204
15443
|
try {
|
|
15205
15444
|
const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
|
|
15206
15445
|
const pkg = JSON.parse(getFileContentSync(pkgPath));
|
|
@@ -15349,9 +15588,10 @@ var FileCheckClient = class {
|
|
|
15349
15588
|
var import_node_fs = require("node:fs");
|
|
15350
15589
|
var import_node_path6 = require("node:path");
|
|
15351
15590
|
init_config();
|
|
15352
|
-
var MODEL_SCHEMA_VERSION = "
|
|
15591
|
+
var MODEL_SCHEMA_VERSION = "v2";
|
|
15353
15592
|
var REQUIRED_MODELS_BY_SCHEMA = {
|
|
15354
|
-
v1: ["pi-model"]
|
|
15593
|
+
v1: ["pi-model"],
|
|
15594
|
+
v2: ["pi-model"]
|
|
15355
15595
|
};
|
|
15356
15596
|
var REQUIRED_MODEL_FILES = {
|
|
15357
15597
|
"pi-model": [
|
|
@@ -15385,8 +15625,11 @@ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
|
15385
15625
|
init_types2();
|
|
15386
15626
|
var STALE_LOCK_MS = 60 * 60 * 1e3;
|
|
15387
15627
|
|
|
15628
|
+
// ../core/dist/clients/model-manifest.js
|
|
15629
|
+
init_config();
|
|
15630
|
+
|
|
15388
15631
|
// ../core/dist/sage-proxy.js
|
|
15389
|
-
function
|
|
15632
|
+
function mapSageHostOs(platform) {
|
|
15390
15633
|
switch (platform) {
|
|
15391
15634
|
case "win32":
|
|
15392
15635
|
return "WINDOWS";
|
|
@@ -15398,21 +15641,37 @@ function mapSageProxyOs(platform) {
|
|
|
15398
15641
|
return platform;
|
|
15399
15642
|
}
|
|
15400
15643
|
}
|
|
15401
|
-
function
|
|
15644
|
+
function mapSageHostArchitecture(arch) {
|
|
15402
15645
|
return arch.toUpperCase();
|
|
15403
15646
|
}
|
|
15647
|
+
function buildSageUserConfig(config2) {
|
|
15648
|
+
return {
|
|
15649
|
+
sensitivity: config2.sensitivity,
|
|
15650
|
+
url_check_enabled: config2.url_check.enabled,
|
|
15651
|
+
file_check_enabled: config2.file_check.enabled,
|
|
15652
|
+
package_check_enabled: config2.package_check.enabled,
|
|
15653
|
+
heuristics_enabled: config2.heuristics_enabled,
|
|
15654
|
+
pi_check_enabled: config2.pi_check.enabled,
|
|
15655
|
+
community_iq_enabled: config2.community_iq,
|
|
15656
|
+
// Optional-chained + defaulted: real callers pass a loadConfig result (skill_check
|
|
15657
|
+
// always present via the schema default), but this feeds fail-open telemetry, so a
|
|
15658
|
+
// partial config must never crash the envelope build.
|
|
15659
|
+
skill_check_upload_enabled: config2.skill_check?.upload_enabled ?? true
|
|
15660
|
+
};
|
|
15661
|
+
}
|
|
15404
15662
|
function buildSageProxyEnvelope(args) {
|
|
15405
15663
|
return {
|
|
15406
15664
|
identity: { uuid: args.iid },
|
|
15407
15665
|
product: { version_app: args.versionApp },
|
|
15408
15666
|
platform: {
|
|
15409
|
-
os: args.platformOs ??
|
|
15410
|
-
architecture: args.platformArchitecture ??
|
|
15667
|
+
os: args.platformOs ?? mapSageHostOs(process.platform),
|
|
15668
|
+
architecture: args.platformArchitecture ?? mapSageHostArchitecture(process.arch)
|
|
15411
15669
|
},
|
|
15412
15670
|
agent: {
|
|
15413
15671
|
agent_runtime: args.agentRuntime,
|
|
15414
15672
|
agent_runtime_version: args.agentRuntimeVersion
|
|
15415
|
-
}
|
|
15673
|
+
},
|
|
15674
|
+
...args.config ? { config: buildSageUserConfig(args.config) } : {}
|
|
15416
15675
|
};
|
|
15417
15676
|
}
|
|
15418
15677
|
|
|
@@ -15920,6 +16179,9 @@ ${content.slice(-tailLen)}`;
|
|
|
15920
16179
|
// ../core/dist/index.js
|
|
15921
16180
|
init_pi_deps_installer();
|
|
15922
16181
|
|
|
16182
|
+
// ../core/dist/clients/skill-analyze.js
|
|
16183
|
+
init_types2();
|
|
16184
|
+
|
|
15923
16185
|
// ../core/dist/clients/skill-check.js
|
|
15924
16186
|
init_types2();
|
|
15925
16187
|
var DEFAULT_TIMEOUT4 = 5;
|
|
@@ -15942,9 +16204,13 @@ var SkillCheckClient = class {
|
|
|
15942
16204
|
/**
|
|
15943
16205
|
* Check a list of skill IDs against the proxy.
|
|
15944
16206
|
*
|
|
15945
|
-
* Returns a Map keyed by skill id
|
|
15946
|
-
*
|
|
15947
|
-
*
|
|
16207
|
+
* Returns a Map keyed by skill id:
|
|
16208
|
+
* - `null` — proxy had no opinion (explicit null in response, or skill absent
|
|
16209
|
+
* from an otherwise-successful response). Callers should treat this as
|
|
16210
|
+
* "unknown, queue for upload".
|
|
16211
|
+
* - absent — the entire batch failed (HTTP error or network error). Callers
|
|
16212
|
+
* should fail open and skip the skill rather than enqueue it.
|
|
16213
|
+
* - object — a verdict from the analyzer.
|
|
15948
16214
|
*
|
|
15949
16215
|
* Fails-open: on any error returns an empty Map. Per-batch errors do
|
|
15950
16216
|
* not poison results from successful batches.
|
|
@@ -15988,8 +16254,10 @@ var SkillCheckClient = class {
|
|
|
15988
16254
|
const data = await response.json();
|
|
15989
16255
|
const results = data.results ?? {};
|
|
15990
16256
|
for (const id of skillIds) {
|
|
15991
|
-
if (!(id in results))
|
|
16257
|
+
if (!(id in results)) {
|
|
16258
|
+
out.set(id, null);
|
|
15992
16259
|
continue;
|
|
16260
|
+
}
|
|
15993
16261
|
const raw = results[id];
|
|
15994
16262
|
if (raw === null || raw === void 0) {
|
|
15995
16263
|
out.set(id, null);
|
|
@@ -16004,17 +16272,10 @@ var SkillCheckClient = class {
|
|
|
16004
16272
|
return out;
|
|
16005
16273
|
}
|
|
16006
16274
|
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
16275
|
return {
|
|
16012
16276
|
skillId,
|
|
16013
16277
|
verdict: typeof raw.verdict === "string" ? raw.verdict : void 0,
|
|
16014
|
-
|
|
16015
|
-
summary: typeof raw.summary === "string" ? raw.summary : void 0,
|
|
16016
|
-
recommendations,
|
|
16017
|
-
threatCategories
|
|
16278
|
+
summary: typeof raw.summary === "string" ? raw.summary : void 0
|
|
16018
16279
|
};
|
|
16019
16280
|
}
|
|
16020
16281
|
};
|
|
@@ -16022,6 +16283,70 @@ var SkillCheckClient = class {
|
|
|
16022
16283
|
// ../core/dist/index.js
|
|
16023
16284
|
init_config();
|
|
16024
16285
|
|
|
16286
|
+
// ../core/dist/config-defaults.js
|
|
16287
|
+
var import_promises3 = require("node:fs/promises");
|
|
16288
|
+
var import_node_path10 = require("node:path");
|
|
16289
|
+
init_file_utils();
|
|
16290
|
+
init_types2();
|
|
16291
|
+
var CONFIG_DEFAULTS_FILENAME = "config.default.json";
|
|
16292
|
+
var CONFIG_DEFAULTS_SCHEMA_VERSION = 1;
|
|
16293
|
+
function buildConfigDefaults() {
|
|
16294
|
+
return {
|
|
16295
|
+
schema_version: CONFIG_DEFAULTS_SCHEMA_VERSION,
|
|
16296
|
+
...ConfigSchema.parse({})
|
|
16297
|
+
};
|
|
16298
|
+
}
|
|
16299
|
+
function serializeConfigDefaults() {
|
|
16300
|
+
return `${JSON.stringify(buildConfigDefaults(), null, 2)}
|
|
16301
|
+
`;
|
|
16302
|
+
}
|
|
16303
|
+
function readSchemaVersion(raw) {
|
|
16304
|
+
try {
|
|
16305
|
+
const data = JSON.parse(raw);
|
|
16306
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
16307
|
+
return null;
|
|
16308
|
+
const version = data.schema_version;
|
|
16309
|
+
return typeof version === "number" && Number.isInteger(version) ? version : null;
|
|
16310
|
+
} catch {
|
|
16311
|
+
return null;
|
|
16312
|
+
}
|
|
16313
|
+
}
|
|
16314
|
+
async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
|
|
16315
|
+
const targetPath = (0, import_node_path10.join)(sageDirPath, CONFIG_DEFAULTS_FILENAME);
|
|
16316
|
+
const defaults = serializeConfigDefaults();
|
|
16317
|
+
try {
|
|
16318
|
+
await (0, import_promises3.mkdir)(sageDirPath, { recursive: true, mode: 448 });
|
|
16319
|
+
await (0, import_promises3.chmod)(sageDirPath, 448);
|
|
16320
|
+
let existing = null;
|
|
16321
|
+
try {
|
|
16322
|
+
existing = await (0, import_promises3.readFile)(targetPath, "utf-8");
|
|
16323
|
+
} catch {
|
|
16324
|
+
}
|
|
16325
|
+
if (existing) {
|
|
16326
|
+
const existingVersion = readSchemaVersion(existing);
|
|
16327
|
+
if (existingVersion !== null && existingVersion > CONFIG_DEFAULTS_SCHEMA_VERSION) {
|
|
16328
|
+
logger.warn("Deployed config defaults are newer, update your installation", {
|
|
16329
|
+
path: targetPath,
|
|
16330
|
+
existingVersion,
|
|
16331
|
+
supportedVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
|
|
16332
|
+
});
|
|
16333
|
+
return;
|
|
16334
|
+
}
|
|
16335
|
+
if (existing === defaults)
|
|
16336
|
+
return;
|
|
16337
|
+
}
|
|
16338
|
+
await withFileLock(targetPath, async () => {
|
|
16339
|
+
await (0, import_promises3.writeFile)(targetPath, defaults, { encoding: "utf-8", mode: 384 });
|
|
16340
|
+
logger.debug("Deployed config defaults", {
|
|
16341
|
+
path: targetPath,
|
|
16342
|
+
schemaVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
|
|
16343
|
+
});
|
|
16344
|
+
});
|
|
16345
|
+
} catch (err) {
|
|
16346
|
+
logger.debug("Failed to deploy config defaults", { path: targetPath, error: String(err) });
|
|
16347
|
+
}
|
|
16348
|
+
}
|
|
16349
|
+
|
|
16025
16350
|
// ../core/dist/config-diagnostics.js
|
|
16026
16351
|
init_config();
|
|
16027
16352
|
init_file_utils();
|
|
@@ -16158,184 +16483,10 @@ function isScannableContent(content) {
|
|
|
16158
16483
|
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
16484
|
}
|
|
16160
16485
|
|
|
16161
|
-
// ../core/dist/content-snapshot.js
|
|
16162
|
-
var import_node_os2 = require("node:os");
|
|
16163
|
-
var CONTENT_FIELD_LIMITS = Object.freeze({
|
|
16164
|
-
command: 512,
|
|
16165
|
-
url: 512,
|
|
16166
|
-
file_path: 512,
|
|
16167
|
-
package_name: 256,
|
|
16168
|
-
package_version: 128,
|
|
16169
|
-
package_registry: 128
|
|
16170
|
-
});
|
|
16171
|
-
function safeTruncate(value, maxLen) {
|
|
16172
|
-
if (maxLen <= 0)
|
|
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;
|
|
16216
|
-
}
|
|
16217
|
-
function extractFirstApplyPatchPath(patchText) {
|
|
16218
|
-
if (!patchText)
|
|
16219
|
-
return void 0;
|
|
16220
|
-
const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
|
|
16221
|
-
if (headerMatch?.[1]) {
|
|
16222
|
-
const trimmed = headerMatch[1].trim();
|
|
16223
|
-
if (trimmed)
|
|
16224
|
-
return trimmed;
|
|
16225
|
-
}
|
|
16226
|
-
const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
|
|
16227
|
-
if (renameMatch?.[1]) {
|
|
16228
|
-
const raw = renameMatch[1].trim();
|
|
16229
|
-
const arrow = raw.indexOf(" -> ");
|
|
16230
|
-
if (arrow !== -1) {
|
|
16231
|
-
const src = raw.slice(0, arrow).trim();
|
|
16232
|
-
if (src)
|
|
16233
|
-
return src;
|
|
16234
|
-
const dst = raw.slice(arrow + 4).trim();
|
|
16235
|
-
if (dst)
|
|
16236
|
-
return dst;
|
|
16237
|
-
} else if (raw) {
|
|
16238
|
-
return raw;
|
|
16239
|
-
}
|
|
16240
|
-
}
|
|
16241
|
-
return void 0;
|
|
16242
|
-
}
|
|
16243
|
-
function extractMcpContent(toolInput) {
|
|
16244
|
-
const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
|
|
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
|
-
};
|
|
16251
|
-
}
|
|
16252
|
-
function firstUrlArtifact(artifacts) {
|
|
16253
|
-
for (const a of artifacts) {
|
|
16254
|
-
if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
|
|
16255
|
-
return a.value;
|
|
16256
|
-
}
|
|
16257
|
-
}
|
|
16258
|
-
return void 0;
|
|
16259
|
-
}
|
|
16260
|
-
function applyFieldLimits(content) {
|
|
16261
|
-
for (const [key, value] of Object.entries(content)) {
|
|
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
|
-
}
|
|
16274
|
-
}
|
|
16275
|
-
function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
|
|
16276
|
-
const content = {};
|
|
16277
|
-
switch (toolType) {
|
|
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;
|
|
16333
|
-
}
|
|
16334
|
-
|
|
16335
16486
|
// ../core/dist/extended-info.js
|
|
16336
|
-
var
|
|
16487
|
+
var import_promises4 = require("node:fs/promises");
|
|
16337
16488
|
var import_node_os3 = require("node:os");
|
|
16338
|
-
var
|
|
16489
|
+
var import_node_path11 = require("node:path");
|
|
16339
16490
|
init_file_utils();
|
|
16340
16491
|
init_types2();
|
|
16341
16492
|
var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
|
|
@@ -16401,8 +16552,8 @@ function sanitizeDocument(parsed, logger) {
|
|
|
16401
16552
|
return out;
|
|
16402
16553
|
}
|
|
16403
16554
|
async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
16404
|
-
const sageDir = sageDirPath ?? (0,
|
|
16405
|
-
const filePath = (0,
|
|
16555
|
+
const sageDir = sageDirPath ?? (0, import_node_path11.join)((0, import_node_os3.homedir)(), ".sage");
|
|
16556
|
+
const filePath = (0, import_node_path11.join)(sageDir, EXTENDED_INFO_FILENAME);
|
|
16406
16557
|
const cached = cache.get(filePath);
|
|
16407
16558
|
if (cached)
|
|
16408
16559
|
return cached.value;
|
|
@@ -16413,7 +16564,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
|
16413
16564
|
async function loadExtendedInfoUncached(filePath, logger) {
|
|
16414
16565
|
let size;
|
|
16415
16566
|
try {
|
|
16416
|
-
const info = await (0,
|
|
16567
|
+
const info = await (0, import_promises4.stat)(filePath);
|
|
16417
16568
|
if (!info.isFile()) {
|
|
16418
16569
|
logger.debug(`extended-info: not a regular file at ${filePath}`);
|
|
16419
16570
|
return null;
|
|
@@ -16501,13 +16652,13 @@ function mergeExtendedInfo(envelope, extendedInfo) {
|
|
|
16501
16652
|
|
|
16502
16653
|
// ../core/dist/installation-id.js
|
|
16503
16654
|
var import_node_crypto4 = require("node:crypto");
|
|
16504
|
-
var
|
|
16505
|
-
var
|
|
16655
|
+
var import_promises5 = require("node:fs/promises");
|
|
16656
|
+
var import_node_path12 = require("node:path");
|
|
16506
16657
|
init_config();
|
|
16507
16658
|
init_file_utils();
|
|
16508
16659
|
async function getInstallationId(sageDirPath) {
|
|
16509
16660
|
const sageDir = sageDirPath ?? resolvePath("~/.sage");
|
|
16510
|
-
const idPath = (0,
|
|
16661
|
+
const idPath = (0, import_node_path12.join)(sageDir, "installation-id");
|
|
16511
16662
|
let fileExists = false;
|
|
16512
16663
|
try {
|
|
16513
16664
|
const existing = await getFileContent(idPath, "utf-8");
|
|
@@ -16519,8 +16670,8 @@ async function getInstallationId(sageDirPath) {
|
|
|
16519
16670
|
}
|
|
16520
16671
|
try {
|
|
16521
16672
|
const id = (0, import_node_crypto4.randomUUID)();
|
|
16522
|
-
await (0,
|
|
16523
|
-
await (0,
|
|
16673
|
+
await (0, import_promises5.mkdir)(sageDir, { recursive: true, mode: 448 });
|
|
16674
|
+
await (0, import_promises5.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
|
|
16524
16675
|
return id;
|
|
16525
16676
|
} catch (err) {
|
|
16526
16677
|
if (err.code === "EEXIST") {
|
|
@@ -16549,10 +16700,10 @@ function resolveTimeoutMs() {
|
|
|
16549
16700
|
const ms = Math.floor(parsed * 1e3);
|
|
16550
16701
|
return Math.min(ms, MAX_EFFECTIVE_TIMEOUT_MS);
|
|
16551
16702
|
}
|
|
16552
|
-
async function
|
|
16703
|
+
async function sendCommunityIqTelemetry(args) {
|
|
16553
16704
|
const logger = args.logger ?? nullLogger;
|
|
16554
16705
|
if (!args.communityIqEnabled) {
|
|
16555
|
-
logger.debug("Community IQ disabled, skipping
|
|
16706
|
+
logger.debug("Community IQ disabled, skipping telemetry");
|
|
16556
16707
|
return;
|
|
16557
16708
|
}
|
|
16558
16709
|
let iid;
|
|
@@ -16561,15 +16712,17 @@ async function sendCommunityIqDetection(args) {
|
|
|
16561
16712
|
} catch {
|
|
16562
16713
|
}
|
|
16563
16714
|
if (!iid) {
|
|
16564
|
-
logger.debug("Skipping
|
|
16715
|
+
logger.debug("Skipping telemetry: missing installation id");
|
|
16565
16716
|
return;
|
|
16566
16717
|
}
|
|
16567
16718
|
const envelope = buildSageProxyEnvelope({
|
|
16568
16719
|
iid,
|
|
16569
16720
|
versionApp: VERSION,
|
|
16570
16721
|
agentRuntime: args.agentRuntime ?? "unknown",
|
|
16571
|
-
agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown"
|
|
16722
|
+
agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown",
|
|
16723
|
+
config: args.config
|
|
16572
16724
|
});
|
|
16725
|
+
const blocking = args.blocking ?? true;
|
|
16573
16726
|
const payload = {
|
|
16574
16727
|
...envelope,
|
|
16575
16728
|
block_event: {
|
|
@@ -16578,8 +16731,8 @@ async function sendCommunityIqDetection(args) {
|
|
|
16578
16731
|
// canonicalize before calling `evaluateToolCall`, so no further
|
|
16579
16732
|
// mapping is needed here.
|
|
16580
16733
|
tool_type: args.toolName,
|
|
16581
|
-
verdict: "deny",
|
|
16582
|
-
user_action: "blocked",
|
|
16734
|
+
verdict: blocking ? "deny" : "suspicious",
|
|
16735
|
+
...blocking ? { user_action: "blocked" } : {},
|
|
16583
16736
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16584
16737
|
...args.signals && Object.keys(args.signals).length > 0 ? { signals: args.signals } : {},
|
|
16585
16738
|
content: args.content ?? {}
|
|
@@ -16590,20 +16743,21 @@ async function sendCommunityIqDetection(args) {
|
|
|
16590
16743
|
const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
|
|
16591
16744
|
const enriched = mergeExtendedInfo(payload, extendedInfo);
|
|
16592
16745
|
const timeoutMs = resolveTimeoutMs();
|
|
16746
|
+
const endpoint = blocking ? "/v2/detection" : "/v2/heuristic";
|
|
16593
16747
|
try {
|
|
16594
|
-
const response = await fetch(resolveEndpoint(
|
|
16748
|
+
const response = await fetch(resolveEndpoint(endpoint), {
|
|
16595
16749
|
method: "POST",
|
|
16596
16750
|
headers: { "Content-Type": "application/json" },
|
|
16597
16751
|
body: JSON.stringify(enriched),
|
|
16598
16752
|
signal: AbortSignal.timeout(timeoutMs)
|
|
16599
16753
|
});
|
|
16600
16754
|
if (!response.ok) {
|
|
16601
|
-
logger.warn("
|
|
16755
|
+
logger.warn("Community IQ telemetry send failed", {
|
|
16602
16756
|
eventId: args.eventId,
|
|
16603
16757
|
status: response.status
|
|
16604
16758
|
});
|
|
16605
16759
|
} else {
|
|
16606
|
-
logger.debug("
|
|
16760
|
+
logger.debug("Community IQ telemetry sent", {
|
|
16607
16761
|
eventId: args.eventId,
|
|
16608
16762
|
toolName: args.toolName,
|
|
16609
16763
|
hookType: args.hookType ?? "PreToolUse"
|
|
@@ -16617,6 +16771,29 @@ async function sendCommunityIqDetection(args) {
|
|
|
16617
16771
|
}
|
|
16618
16772
|
}
|
|
16619
16773
|
|
|
16774
|
+
// ../core/dist/e2e-capture.js
|
|
16775
|
+
var import_node_path13 = require("node:path");
|
|
16776
|
+
var CAPTURE_MAX_BYTES = 10 * 1024 * 1024;
|
|
16777
|
+
var CAPTURE_MAX_FILES = 2;
|
|
16778
|
+
function captureEnabled() {
|
|
16779
|
+
return !!process.env.SAGE_E2E_CAPTURE_DIR;
|
|
16780
|
+
}
|
|
16781
|
+
async function captureHookInput(event, raw, normalized, options = {}) {
|
|
16782
|
+
const dir = process.env.SAGE_E2E_CAPTURE_DIR;
|
|
16783
|
+
if (!dir)
|
|
16784
|
+
return;
|
|
16785
|
+
try {
|
|
16786
|
+
const prefix = options.filePrefix ?? "";
|
|
16787
|
+
const base = event === "PreToolUse" ? "pre-tool-use.jsonl" : "post-tool-use.jsonl";
|
|
16788
|
+
await appendJsonlEntry({
|
|
16789
|
+
path: (0, import_node_path13.join)(dir, `${prefix}${base}`),
|
|
16790
|
+
max_bytes: CAPTURE_MAX_BYTES,
|
|
16791
|
+
max_files: CAPTURE_MAX_FILES
|
|
16792
|
+
}, { event, raw, normalized });
|
|
16793
|
+
} catch {
|
|
16794
|
+
}
|
|
16795
|
+
}
|
|
16796
|
+
|
|
16620
16797
|
// ../core/dist/policy.js
|
|
16621
16798
|
init_types2();
|
|
16622
16799
|
var SENSITIVITY_POLICY = {
|
|
@@ -16840,27 +17017,27 @@ init_config();
|
|
|
16840
17017
|
|
|
16841
17018
|
// ../core/dist/exceptions.js
|
|
16842
17019
|
var import_node_crypto5 = require("node:crypto");
|
|
16843
|
-
var
|
|
17020
|
+
var import_node_path15 = require("node:path");
|
|
16844
17021
|
init_config();
|
|
16845
17022
|
init_file_utils();
|
|
16846
17023
|
|
|
16847
17024
|
// ../core/dist/trusted-domains.js
|
|
16848
|
-
var
|
|
16849
|
-
var
|
|
17025
|
+
var import_promises6 = require("node:fs/promises");
|
|
17026
|
+
var import_node_path14 = require("node:path");
|
|
16850
17027
|
var import_yaml = __toESM(require_dist(), 1);
|
|
16851
17028
|
init_file_utils();
|
|
16852
17029
|
init_types2();
|
|
16853
17030
|
async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
|
|
16854
17031
|
let files;
|
|
16855
17032
|
try {
|
|
16856
|
-
files = (await (0,
|
|
17033
|
+
files = (await (0, import_promises6.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
16857
17034
|
} catch {
|
|
16858
17035
|
logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
|
|
16859
17036
|
return [];
|
|
16860
17037
|
}
|
|
16861
17038
|
const domains = [];
|
|
16862
17039
|
for (const filename of files) {
|
|
16863
|
-
const filePath = (0,
|
|
17040
|
+
const filePath = (0, import_node_path14.join)(trustedDomainsDir, filename);
|
|
16864
17041
|
let content;
|
|
16865
17042
|
try {
|
|
16866
17043
|
content = await getFileContent(filePath);
|
|
@@ -17136,7 +17313,7 @@ function matchesDomain(pattern, url) {
|
|
|
17136
17313
|
function normalizePatternPath(pattern) {
|
|
17137
17314
|
const home = getHomeDir();
|
|
17138
17315
|
const expanded = pattern.startsWith("~/") || pattern === "~" ? `${home}${pattern.slice(1)}` : pattern;
|
|
17139
|
-
return (0,
|
|
17316
|
+
return (0, import_node_path15.normalize)(expanded);
|
|
17140
17317
|
}
|
|
17141
17318
|
function matchesPath(pattern, filePath) {
|
|
17142
17319
|
const hasWildcard = pattern.includes("*");
|
|
@@ -17147,7 +17324,7 @@ function matchesPath(pattern, filePath) {
|
|
|
17147
17324
|
const normalizedPath = normalizePatternPath(filePath);
|
|
17148
17325
|
if (normalizedPath === normalizedPattern)
|
|
17149
17326
|
return true;
|
|
17150
|
-
if (normalizedPath.startsWith(normalizedPattern +
|
|
17327
|
+
if (normalizedPath.startsWith(normalizedPattern + import_node_path15.sep))
|
|
17151
17328
|
return true;
|
|
17152
17329
|
return false;
|
|
17153
17330
|
}
|
|
@@ -17927,7 +18104,7 @@ function extractFromRequirementsTxt(content) {
|
|
|
17927
18104
|
}
|
|
17928
18105
|
|
|
17929
18106
|
// ../core/dist/statusline.js
|
|
17930
|
-
var
|
|
18107
|
+
var import_node_path16 = require("node:path");
|
|
17931
18108
|
init_config();
|
|
17932
18109
|
init_file_utils();
|
|
17933
18110
|
var STATUS_PREFIX = "statusline-";
|
|
@@ -17936,15 +18113,17 @@ function sanitizeSessionId(sessionId) {
|
|
|
17936
18113
|
return sessionId.replace(/[^a-zA-Z0-9-]/g, "_");
|
|
17937
18114
|
}
|
|
17938
18115
|
function statusFilePath(sessionId) {
|
|
17939
|
-
return (0,
|
|
18116
|
+
return (0, import_node_path16.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
|
|
17940
18117
|
}
|
|
17941
18118
|
function emptyStatus() {
|
|
18119
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17942
18120
|
return {
|
|
17943
18121
|
denied: 0,
|
|
17944
18122
|
flagged: 0,
|
|
17945
18123
|
lastCategory: null,
|
|
17946
18124
|
lastReason: null,
|
|
17947
|
-
updatedAt:
|
|
18125
|
+
updatedAt: now,
|
|
18126
|
+
startedAt: now
|
|
17948
18127
|
};
|
|
17949
18128
|
}
|
|
17950
18129
|
async function readStatus(sessionId) {
|
|
@@ -17969,8 +18148,8 @@ async function updateSessionStatus(sessionId, verdict) {
|
|
|
17969
18148
|
}
|
|
17970
18149
|
|
|
17971
18150
|
// ../core/dist/threat-loader.js
|
|
17972
|
-
var
|
|
17973
|
-
var
|
|
18151
|
+
var import_promises7 = require("node:fs/promises");
|
|
18152
|
+
var import_node_path17 = require("node:path");
|
|
17974
18153
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
17975
18154
|
init_file_utils();
|
|
17976
18155
|
init_types2();
|
|
@@ -18003,13 +18182,13 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18003
18182
|
const threats = [];
|
|
18004
18183
|
let files;
|
|
18005
18184
|
try {
|
|
18006
|
-
files = (await (0,
|
|
18185
|
+
files = (await (0, import_promises7.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
18007
18186
|
} catch {
|
|
18008
18187
|
logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
|
|
18009
18188
|
return threats;
|
|
18010
18189
|
}
|
|
18011
18190
|
for (const filename of files) {
|
|
18012
|
-
const filePath = (0,
|
|
18191
|
+
const filePath = (0, import_node_path17.join)(threatDir, filename);
|
|
18013
18192
|
let content;
|
|
18014
18193
|
try {
|
|
18015
18194
|
content = await getFileContent(filePath);
|
|
@@ -18365,8 +18544,8 @@ async function evaluateToolCall(request, context) {
|
|
|
18365
18544
|
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
|
|
18366
18545
|
piCheckResults: allPiResults,
|
|
18367
18546
|
piThresholds: {
|
|
18368
|
-
highRisk:
|
|
18369
|
-
mediumRisk:
|
|
18547
|
+
highRisk: DEFAULT_PI_HIGH_RISK_THRESHOLD,
|
|
18548
|
+
mediumRisk: DEFAULT_PI_MEDIUM_RISK_THRESHOLD
|
|
18370
18549
|
}
|
|
18371
18550
|
});
|
|
18372
18551
|
} else {
|
|
@@ -18457,7 +18636,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18457
18636
|
}
|
|
18458
18637
|
}
|
|
18459
18638
|
if (allPiResults.length > 0) {
|
|
18460
|
-
const piSnippetFloor =
|
|
18639
|
+
const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
|
|
18461
18640
|
auditSignals.pi_checks = allPiResults.map((r) => ({
|
|
18462
18641
|
risk: r.risk,
|
|
18463
18642
|
model_id: r.modelId,
|
|
@@ -18491,9 +18670,9 @@ async function evaluateToolCall(request, context) {
|
|
|
18491
18670
|
} catch (error) {
|
|
18492
18671
|
logger.debug("Audit verdict logging failed open", { error: String(error) });
|
|
18493
18672
|
}
|
|
18494
|
-
|
|
18673
|
+
const submitTelemetry = async (blocking) => {
|
|
18495
18674
|
try {
|
|
18496
|
-
await
|
|
18675
|
+
await sendCommunityIqTelemetry({
|
|
18497
18676
|
eventId,
|
|
18498
18677
|
agentRuntime: request.agentRuntime,
|
|
18499
18678
|
agentRuntimeVersion: request.agentRuntimeVersion,
|
|
@@ -18502,11 +18681,18 @@ async function evaluateToolCall(request, context) {
|
|
|
18502
18681
|
content: resolvedContent,
|
|
18503
18682
|
signals: resolvedSignals,
|
|
18504
18683
|
communityIqEnabled: config2.community_iq,
|
|
18684
|
+
config: config2,
|
|
18685
|
+
blocking,
|
|
18505
18686
|
logger
|
|
18506
18687
|
});
|
|
18507
18688
|
} catch (error) {
|
|
18508
|
-
logger.debug("
|
|
18689
|
+
logger.debug("Community IQ telemetry failed open", { error: String(error) });
|
|
18509
18690
|
}
|
|
18691
|
+
};
|
|
18692
|
+
if (verdict.decision === "deny") {
|
|
18693
|
+
await submitTelemetry(true);
|
|
18694
|
+
} else if (allPiResults.some((r) => r.risk >= DEFAULT_PI_TELEMETRY_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD)) {
|
|
18695
|
+
await submitTelemetry(false);
|
|
18510
18696
|
}
|
|
18511
18697
|
if (verdict.decision !== "allow") {
|
|
18512
18698
|
try {
|
|
@@ -18515,7 +18701,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18515
18701
|
logger.debug("Session status update failed open", { error: String(error) });
|
|
18516
18702
|
}
|
|
18517
18703
|
}
|
|
18518
|
-
const piWarnings = allPiResults.filter((r) => r.risk >=
|
|
18704
|
+
const piWarnings = allPiResults.filter((r) => r.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD);
|
|
18519
18705
|
if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
|
|
18520
18706
|
verdict.piWarnings = piWarnings;
|
|
18521
18707
|
logEvaluationCompleted(verdict);
|
|
@@ -18657,7 +18843,7 @@ function createPiProvider(config2, _context, logger) {
|
|
|
18657
18843
|
return new BundledPiProvider({
|
|
18658
18844
|
modelPath: config2.pi_check.model_path,
|
|
18659
18845
|
maxContentLength: config2.pi_check.max_content_length,
|
|
18660
|
-
mediumRiskThreshold:
|
|
18846
|
+
mediumRiskThreshold: DEFAULT_PI_MEDIUM_RISK_THRESHOLD,
|
|
18661
18847
|
logger
|
|
18662
18848
|
});
|
|
18663
18849
|
}
|
|
@@ -18837,18 +19023,226 @@ function formatDenyMessage(verdict, branding2 = defaultBranding) {
|
|
|
18837
19023
|
].join("\n");
|
|
18838
19024
|
}
|
|
18839
19025
|
|
|
18840
|
-
// ../core/dist/
|
|
18841
|
-
|
|
19026
|
+
// ../core/dist/install-state.js
|
|
19027
|
+
var import_node_path18 = require("node:path");
|
|
19028
|
+
init_config();
|
|
19029
|
+
init_file_utils();
|
|
18842
19030
|
|
|
18843
|
-
// ../core/dist/
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
19031
|
+
// ../core/dist/notices.js
|
|
19032
|
+
init_config();
|
|
19033
|
+
var SKILL_UPLOAD_NOTICE = {
|
|
19034
|
+
id: "skill_upload_v1",
|
|
19035
|
+
body: () => [
|
|
19036
|
+
"unknown skills \u2014 each skill's SKILL.md and every supporting file in its",
|
|
19037
|
+
"folder \u2014 will be uploaded and checked against potential malicious artifacts",
|
|
19038
|
+
"starting next session. To keep skill content on this machine, set",
|
|
19039
|
+
'"skill_check": { "upload_enabled": false } in ~/.sage/config.json.'
|
|
19040
|
+
],
|
|
19041
|
+
// Suppressed once the user has explicitly set upload_enabled either way: an
|
|
19042
|
+
// explicit choice needs no consent prompt (the rollout obeys it directly).
|
|
19043
|
+
isEligible: async ({ configPath, logger }) => !(await readExplicitSkillUploadEnabled(configPath, logger)).present
|
|
18849
19044
|
};
|
|
18850
|
-
var
|
|
18851
|
-
|
|
19045
|
+
var NOTICES = {
|
|
19046
|
+
[SKILL_UPLOAD_NOTICE.id]: SKILL_UPLOAD_NOTICE
|
|
19047
|
+
};
|
|
19048
|
+
var INFO_ICON = "\u2139\uFE0F";
|
|
19049
|
+
var CONTINUATION_INDENT = " ";
|
|
19050
|
+
function formatNotice(notice, branding2 = defaultBranding) {
|
|
19051
|
+
const [first = "", ...rest] = notice.body(branding2);
|
|
19052
|
+
const head = `${INFO_ICON} ${branding2.name}: ${first}`;
|
|
19053
|
+
return [head, ...rest.map((line) => `${CONTINUATION_INDENT}${line}`)].join("\n");
|
|
19054
|
+
}
|
|
19055
|
+
function formatNoticeById(id, branding2 = defaultBranding) {
|
|
19056
|
+
const notice = NOTICES[id];
|
|
19057
|
+
return notice ? formatNotice(notice, branding2) : "";
|
|
19058
|
+
}
|
|
19059
|
+
|
|
19060
|
+
// ../core/dist/install-state.js
|
|
19061
|
+
init_types2();
|
|
19062
|
+
var INSTALL_STATE_SCHEMA_VERSION = 1;
|
|
19063
|
+
var SKILL_UPLOAD_NOTICE_ID = SKILL_UPLOAD_NOTICE.id;
|
|
19064
|
+
function defaultStatePath(sageDirPath) {
|
|
19065
|
+
return (0, import_node_path18.join)(sageDirPath ?? resolvePath(SAGE_DIR), "install-state.json");
|
|
19066
|
+
}
|
|
19067
|
+
function emptyState() {
|
|
19068
|
+
return { schemaVersion: INSTALL_STATE_SCHEMA_VERSION, notices: {} };
|
|
19069
|
+
}
|
|
19070
|
+
async function loadInstallState(sageDirPath, logger = nullLogger) {
|
|
19071
|
+
let raw;
|
|
19072
|
+
try {
|
|
19073
|
+
raw = await getFileContent(defaultStatePath(sageDirPath));
|
|
19074
|
+
} catch {
|
|
19075
|
+
return emptyState();
|
|
19076
|
+
}
|
|
19077
|
+
try {
|
|
19078
|
+
const data = JSON.parse(raw);
|
|
19079
|
+
const rawNotices = data.notices ?? {};
|
|
19080
|
+
const notices = {};
|
|
19081
|
+
for (const [id, rec] of Object.entries(rawNotices)) {
|
|
19082
|
+
notices[id] = {
|
|
19083
|
+
noticed: rec.noticed === true,
|
|
19084
|
+
version: typeof rec.version === "string" ? rec.version : void 0
|
|
19085
|
+
};
|
|
19086
|
+
}
|
|
19087
|
+
return {
|
|
19088
|
+
schemaVersion: typeof data.schema_version === "number" ? data.schema_version : INSTALL_STATE_SCHEMA_VERSION,
|
|
19089
|
+
lastRunVersion: typeof data.last_run_version === "string" ? data.last_run_version : void 0,
|
|
19090
|
+
notices
|
|
19091
|
+
};
|
|
19092
|
+
} catch (e) {
|
|
19093
|
+
logger.warn("Failed to parse install state; treating as fresh", { error: String(e) });
|
|
19094
|
+
return emptyState();
|
|
19095
|
+
}
|
|
19096
|
+
}
|
|
19097
|
+
async function saveInstallState(state, sageDirPath, logger = nullLogger) {
|
|
19098
|
+
try {
|
|
19099
|
+
const data = {
|
|
19100
|
+
schema_version: state.schemaVersion,
|
|
19101
|
+
last_run_version: state.lastRunVersion,
|
|
19102
|
+
notices: Object.fromEntries(Object.entries(state.notices).map(([id, rec]) => [
|
|
19103
|
+
id,
|
|
19104
|
+
{ noticed: rec.noticed, version: rec.version }
|
|
19105
|
+
]))
|
|
19106
|
+
};
|
|
19107
|
+
await atomicWriteJson(defaultStatePath(sageDirPath), data);
|
|
19108
|
+
} catch (e) {
|
|
19109
|
+
logger.warn("Failed to save install state", { error: String(e) });
|
|
19110
|
+
}
|
|
19111
|
+
}
|
|
19112
|
+
function decideSkillUploadRollout(state, opts = {}) {
|
|
19113
|
+
const versionChanged = opts.version !== void 0 && state.lastRunVersion !== opts.version;
|
|
19114
|
+
const withVersion = () => ({ ...state, lastRunVersion: opts.version });
|
|
19115
|
+
if (opts.explicitConfig) {
|
|
19116
|
+
return {
|
|
19117
|
+
uploadActive: opts.explicitValue === true,
|
|
19118
|
+
showNotice: false,
|
|
19119
|
+
nextState: versionChanged ? withVersion() : null
|
|
19120
|
+
};
|
|
19121
|
+
}
|
|
19122
|
+
const notice = state.notices[SKILL_UPLOAD_NOTICE_ID];
|
|
19123
|
+
if (notice?.noticed === true) {
|
|
19124
|
+
return {
|
|
19125
|
+
uploadActive: true,
|
|
19126
|
+
showNotice: false,
|
|
19127
|
+
nextState: versionChanged ? withVersion() : null
|
|
19128
|
+
};
|
|
19129
|
+
}
|
|
19130
|
+
const nextState = {
|
|
19131
|
+
...state,
|
|
19132
|
+
lastRunVersion: opts.version ?? state.lastRunVersion,
|
|
19133
|
+
notices: {
|
|
19134
|
+
...state.notices,
|
|
19135
|
+
[SKILL_UPLOAD_NOTICE_ID]: { noticed: true, version: opts.version }
|
|
19136
|
+
}
|
|
19137
|
+
};
|
|
19138
|
+
return { uploadActive: false, showNotice: true, nextState };
|
|
19139
|
+
}
|
|
19140
|
+
var processRollout;
|
|
19141
|
+
async function resolveSkillUploadRollout(args) {
|
|
19142
|
+
const commitNotice = args.commitNotice ?? true;
|
|
19143
|
+
const logger = args.logger ?? nullLogger;
|
|
19144
|
+
let explicit;
|
|
19145
|
+
try {
|
|
19146
|
+
explicit = await readExplicitSkillUploadEnabled(args.configPath, logger);
|
|
19147
|
+
} catch {
|
|
19148
|
+
explicit = { present: false, value: false };
|
|
19149
|
+
}
|
|
19150
|
+
if (explicit.present) {
|
|
19151
|
+
const resolved = {
|
|
19152
|
+
uploadActive: explicit.value === true,
|
|
19153
|
+
showNotice: false,
|
|
19154
|
+
deferUnknownSkills: false
|
|
19155
|
+
};
|
|
19156
|
+
logger.debug("Skill-upload rollout resolved", {
|
|
19157
|
+
explicitConfig: true,
|
|
19158
|
+
explicitValue: explicit.value,
|
|
19159
|
+
uploadActive: resolved.uploadActive,
|
|
19160
|
+
showNotice: false,
|
|
19161
|
+
deferUnknownSkills: false,
|
|
19162
|
+
commitNotice,
|
|
19163
|
+
statePersisted: false,
|
|
19164
|
+
version: args.version
|
|
19165
|
+
});
|
|
19166
|
+
return resolved;
|
|
19167
|
+
}
|
|
19168
|
+
if (processRollout)
|
|
19169
|
+
return processRollout;
|
|
19170
|
+
try {
|
|
19171
|
+
const state = await loadInstallState(args.sageDirPath, logger);
|
|
19172
|
+
const decision = decideSkillUploadRollout(state, { version: args.version });
|
|
19173
|
+
if (commitNotice) {
|
|
19174
|
+
if (decision.nextState) {
|
|
19175
|
+
await saveInstallState(decision.nextState, args.sageDirPath, logger);
|
|
19176
|
+
}
|
|
19177
|
+
processRollout = {
|
|
19178
|
+
uploadActive: decision.uploadActive,
|
|
19179
|
+
showNotice: decision.showNotice,
|
|
19180
|
+
deferUnknownSkills: decision.showNotice
|
|
19181
|
+
};
|
|
19182
|
+
} else {
|
|
19183
|
+
processRollout = {
|
|
19184
|
+
uploadActive: decision.uploadActive,
|
|
19185
|
+
showNotice: false,
|
|
19186
|
+
deferUnknownSkills: decision.showNotice
|
|
19187
|
+
};
|
|
19188
|
+
}
|
|
19189
|
+
logger.debug("Skill-upload rollout resolved", {
|
|
19190
|
+
explicitConfig: false,
|
|
19191
|
+
uploadActive: processRollout.uploadActive,
|
|
19192
|
+
showNotice: processRollout.showNotice,
|
|
19193
|
+
deferUnknownSkills: processRollout.deferUnknownSkills,
|
|
19194
|
+
commitNotice,
|
|
19195
|
+
statePersisted: commitNotice && decision.nextState !== null,
|
|
19196
|
+
version: args.version
|
|
19197
|
+
});
|
|
19198
|
+
} catch (e) {
|
|
19199
|
+
logger.warn("Skill-upload rollout resolution failed; suppressing upload", {
|
|
19200
|
+
error: String(e)
|
|
19201
|
+
});
|
|
19202
|
+
processRollout = { uploadActive: false, showNotice: commitNotice, deferUnknownSkills: true };
|
|
19203
|
+
}
|
|
19204
|
+
return processRollout;
|
|
19205
|
+
}
|
|
19206
|
+
async function takePendingNotices(args) {
|
|
19207
|
+
const logger = args.logger ?? nullLogger;
|
|
19208
|
+
try {
|
|
19209
|
+
const state = await loadInstallState(args.sageDirPath, logger);
|
|
19210
|
+
const toShow = [];
|
|
19211
|
+
for (const notice of Object.values(NOTICES)) {
|
|
19212
|
+
if (state.notices[notice.id]?.noticed === true)
|
|
19213
|
+
continue;
|
|
19214
|
+
const eligible = notice.isEligible ? await notice.isEligible({ configPath: args.configPath, logger }) : true;
|
|
19215
|
+
if (eligible)
|
|
19216
|
+
toShow.push(notice.id);
|
|
19217
|
+
}
|
|
19218
|
+
if (toShow.length === 0)
|
|
19219
|
+
return [];
|
|
19220
|
+
const nextNotices = { ...state.notices };
|
|
19221
|
+
for (const id of toShow)
|
|
19222
|
+
nextNotices[id] = { noticed: true };
|
|
19223
|
+
await saveInstallState({ ...state, notices: nextNotices }, args.sageDirPath, logger);
|
|
19224
|
+
logger.debug("Pending notices taken for delivery", { noticeIds: toShow });
|
|
19225
|
+
return toShow;
|
|
19226
|
+
} catch (e) {
|
|
19227
|
+
logger.warn("takePendingNotices failed; showing no notices this turn", {
|
|
19228
|
+
error: String(e)
|
|
19229
|
+
});
|
|
19230
|
+
return [];
|
|
19231
|
+
}
|
|
19232
|
+
}
|
|
19233
|
+
|
|
19234
|
+
// ../core/dist/model-download.js
|
|
19235
|
+
init_types2();
|
|
19236
|
+
|
|
19237
|
+
// ../core/dist/operational-log.js
|
|
19238
|
+
var LEVEL_PRIORITY = {
|
|
19239
|
+
debug: 10,
|
|
19240
|
+
info: 20,
|
|
19241
|
+
warn: 30,
|
|
19242
|
+
error: 40
|
|
19243
|
+
};
|
|
19244
|
+
var FLUSH_TIMEOUT_MS = 250;
|
|
19245
|
+
function shouldLog(config2, level) {
|
|
18852
19246
|
return config2.enabled && LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[config2.level];
|
|
18853
19247
|
}
|
|
18854
19248
|
function normalizeData(data) {
|
|
@@ -18908,8 +19302,8 @@ function createOperationalLogger(config2, runtime) {
|
|
|
18908
19302
|
try {
|
|
18909
19303
|
const result = await Promise.race([
|
|
18910
19304
|
Promise.allSettled([...pendingWrites]).then(() => "drained"),
|
|
18911
|
-
new Promise((
|
|
18912
|
-
timeout = setTimeout(() =>
|
|
19305
|
+
new Promise((resolve10) => {
|
|
19306
|
+
timeout = setTimeout(() => resolve10("timeout"), remainingMs);
|
|
18913
19307
|
timeout.unref?.();
|
|
18914
19308
|
})
|
|
18915
19309
|
]);
|
|
@@ -18958,13 +19352,14 @@ function createOperationalLogger(config2, runtime) {
|
|
|
18958
19352
|
|
|
18959
19353
|
// ../core/dist/plugin-scan-cache.js
|
|
18960
19354
|
var import_node_crypto7 = require("node:crypto");
|
|
18961
|
-
var
|
|
18962
|
-
var
|
|
19355
|
+
var import_promises8 = require("node:fs/promises");
|
|
19356
|
+
var import_node_path19 = require("node:path");
|
|
19357
|
+
init_config();
|
|
18963
19358
|
init_file_utils();
|
|
18964
19359
|
init_types2();
|
|
18965
|
-
var DEFAULT_CACHE_PATH = (0,
|
|
18966
|
-
var
|
|
18967
|
-
var SCHEMA_VERSION =
|
|
19360
|
+
var DEFAULT_CACHE_PATH = (0, import_node_path19.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
|
|
19361
|
+
var DEFAULT_CACHE_TTL_MS = MS_PER_DAY;
|
|
19362
|
+
var SCHEMA_VERSION = 5;
|
|
18968
19363
|
function cacheKey(pluginKey, version, lastUpdated) {
|
|
18969
19364
|
return `${pluginKey}:${version}:${lastUpdated}`;
|
|
18970
19365
|
}
|
|
@@ -18975,13 +19370,13 @@ async function computeConfigHash(sageVersion, ...dirs) {
|
|
|
18975
19370
|
for (const dir of dirs) {
|
|
18976
19371
|
let files;
|
|
18977
19372
|
try {
|
|
18978
|
-
files = (await (0,
|
|
19373
|
+
files = (await (0, import_promises8.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
18979
19374
|
} catch {
|
|
18980
19375
|
continue;
|
|
18981
19376
|
}
|
|
18982
19377
|
for (const file of files) {
|
|
18983
19378
|
try {
|
|
18984
|
-
const content = await getFileContent((0,
|
|
19379
|
+
const content = await getFileContent((0, import_node_path19.join)(dir, file));
|
|
18985
19380
|
h.update(content);
|
|
18986
19381
|
} catch {
|
|
18987
19382
|
}
|
|
@@ -19050,7 +19445,7 @@ async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nu
|
|
|
19050
19445
|
logger.warn(`Failed to save scan cache to ${cachePath}`, { error: String(e) });
|
|
19051
19446
|
}
|
|
19052
19447
|
}
|
|
19053
|
-
function isCached(cache2, pluginKey, version, lastUpdated) {
|
|
19448
|
+
function isCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
|
|
19054
19449
|
const key = cacheKey(pluginKey, version, lastUpdated);
|
|
19055
19450
|
const entry = cache2.entries[key];
|
|
19056
19451
|
if (!entry)
|
|
@@ -19058,13 +19453,13 @@ function isCached(cache2, pluginKey, version, lastUpdated) {
|
|
|
19058
19453
|
try {
|
|
19059
19454
|
const scannedAt = new Date(entry.scannedAt);
|
|
19060
19455
|
const age = Date.now() - scannedAt.getTime();
|
|
19061
|
-
return age <
|
|
19456
|
+
return age < ttlMs;
|
|
19062
19457
|
} catch {
|
|
19063
19458
|
return false;
|
|
19064
19459
|
}
|
|
19065
19460
|
}
|
|
19066
|
-
function getCached(cache2, pluginKey, version, lastUpdated) {
|
|
19067
|
-
if (!isCached(cache2, pluginKey, version, lastUpdated))
|
|
19461
|
+
function getCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
|
|
19462
|
+
if (!isCached(cache2, pluginKey, version, lastUpdated, ttlMs))
|
|
19068
19463
|
return null;
|
|
19069
19464
|
const key = cacheKey(pluginKey, version, lastUpdated);
|
|
19070
19465
|
return cache2.entries[key] ?? null;
|
|
@@ -19081,18 +19476,21 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
|
|
|
19081
19476
|
|
|
19082
19477
|
// ../core/dist/plugin-scanner.js
|
|
19083
19478
|
var import_node_crypto9 = require("node:crypto");
|
|
19084
|
-
var
|
|
19085
|
-
var
|
|
19479
|
+
var import_promises11 = require("node:fs/promises");
|
|
19480
|
+
var import_node_path23 = require("node:path");
|
|
19086
19481
|
init_config();
|
|
19087
19482
|
init_file_utils();
|
|
19088
19483
|
|
|
19089
19484
|
// ../core/dist/skill-id.js
|
|
19090
19485
|
var import_node_crypto8 = require("node:crypto");
|
|
19091
|
-
var
|
|
19092
|
-
var
|
|
19486
|
+
var import_promises9 = require("node:fs/promises");
|
|
19487
|
+
var import_node_path20 = require("node:path");
|
|
19093
19488
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
|
|
19489
|
+
var MAX_SKILL_BYTES = 50 * 1024 * 1024;
|
|
19490
|
+
var SkillTooLargeError = class extends Error {
|
|
19491
|
+
};
|
|
19094
19492
|
function isContained(childReal, rootReal) {
|
|
19095
|
-
return childReal === rootReal || childReal.startsWith(`${rootReal}${
|
|
19493
|
+
return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path20.sep}`);
|
|
19096
19494
|
}
|
|
19097
19495
|
function normalizePath(p) {
|
|
19098
19496
|
let out = p.replace(/\\/g, "/").normalize("NFC");
|
|
@@ -19102,47 +19500,51 @@ function normalizePath(p) {
|
|
|
19102
19500
|
}
|
|
19103
19501
|
return out;
|
|
19104
19502
|
}
|
|
19105
|
-
async function entriesFromDirectory(dirPath) {
|
|
19106
|
-
const absDir = (0,
|
|
19107
|
-
const rootReal = await (0,
|
|
19503
|
+
async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY) {
|
|
19504
|
+
const absDir = (0, import_node_path20.resolve)(dirPath);
|
|
19505
|
+
const rootReal = await (0, import_promises9.realpath)(absDir);
|
|
19108
19506
|
const entries = [];
|
|
19109
19507
|
const visited = /* @__PURE__ */ new Set();
|
|
19508
|
+
let total = 0;
|
|
19110
19509
|
async function walk(currentPath) {
|
|
19111
|
-
const real = await (0,
|
|
19510
|
+
const real = await (0, import_promises9.realpath)(currentPath);
|
|
19112
19511
|
if (visited.has(real))
|
|
19113
19512
|
return;
|
|
19114
19513
|
if (!isContained(real, rootReal))
|
|
19115
19514
|
return;
|
|
19116
19515
|
visited.add(real);
|
|
19117
|
-
const items = await (0,
|
|
19516
|
+
const items = await (0, import_promises9.readdir)(currentPath);
|
|
19118
19517
|
const dirs = [];
|
|
19119
19518
|
const files = [];
|
|
19120
19519
|
for (const item of items) {
|
|
19121
|
-
const fullPath = (0,
|
|
19122
|
-
const st = await (0,
|
|
19520
|
+
const fullPath = (0, import_node_path20.join)(currentPath, item);
|
|
19521
|
+
const st = await (0, import_promises9.stat)(fullPath);
|
|
19123
19522
|
if (st.isDirectory())
|
|
19124
19523
|
dirs.push(item);
|
|
19125
19524
|
else if (st.isFile())
|
|
19126
|
-
files.push(item);
|
|
19525
|
+
files.push({ name: item, size: st.size });
|
|
19127
19526
|
}
|
|
19128
19527
|
dirs.sort();
|
|
19129
|
-
files.sort();
|
|
19528
|
+
files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
19130
19529
|
for (const d of dirs) {
|
|
19131
|
-
const fullPath = (0,
|
|
19132
|
-
const relPath = (0,
|
|
19530
|
+
const fullPath = (0, import_node_path20.join)(currentPath, d);
|
|
19531
|
+
const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19133
19532
|
entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
|
|
19134
19533
|
await walk(fullPath);
|
|
19135
19534
|
}
|
|
19136
19535
|
for (const f of files) {
|
|
19137
|
-
const fullPath = (0,
|
|
19138
|
-
const fileReal = await (0,
|
|
19536
|
+
const fullPath = (0, import_node_path20.join)(currentPath, f.name);
|
|
19537
|
+
const fileReal = await (0, import_promises9.realpath)(fullPath);
|
|
19139
19538
|
if (!isContained(fileReal, rootReal))
|
|
19140
19539
|
continue;
|
|
19141
|
-
|
|
19540
|
+
total += f.size;
|
|
19541
|
+
if (total > maxBytes)
|
|
19542
|
+
throw new SkillTooLargeError();
|
|
19543
|
+
const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19142
19544
|
entries.push({
|
|
19143
19545
|
entryPath: relPath,
|
|
19144
19546
|
isDir: false,
|
|
19145
|
-
content: await (0,
|
|
19547
|
+
content: await (0, import_promises9.readFile)(fullPath)
|
|
19146
19548
|
});
|
|
19147
19549
|
}
|
|
19148
19550
|
}
|
|
@@ -19179,77 +19581,343 @@ function computeSkillId(entries) {
|
|
|
19179
19581
|
}
|
|
19180
19582
|
return { skillId: skillHasher.digest("hex"), fileHashes };
|
|
19181
19583
|
}
|
|
19182
|
-
async function
|
|
19584
|
+
async function findSkillPackagesWithMtime(rootDir) {
|
|
19183
19585
|
const found = [];
|
|
19184
19586
|
let rootReal;
|
|
19185
19587
|
try {
|
|
19186
|
-
|
|
19187
|
-
if (!rootStat.isDirectory())
|
|
19188
|
-
return found;
|
|
19189
|
-
rootReal = await (0, import_promises8.realpath)((0, import_node_path17.resolve)(rootDir));
|
|
19588
|
+
rootReal = await (0, import_promises9.realpath)((0, import_node_path20.resolve)(rootDir));
|
|
19190
19589
|
} catch {
|
|
19191
19590
|
return found;
|
|
19192
19591
|
}
|
|
19193
19592
|
const visited = /* @__PURE__ */ new Set();
|
|
19194
|
-
async function walk(
|
|
19593
|
+
async function walk(currentPath) {
|
|
19195
19594
|
let real;
|
|
19196
19595
|
try {
|
|
19197
|
-
real = await (0,
|
|
19596
|
+
real = await (0, import_promises9.realpath)(currentPath);
|
|
19198
19597
|
} catch {
|
|
19199
|
-
return;
|
|
19598
|
+
return 0;
|
|
19200
19599
|
}
|
|
19201
19600
|
if (visited.has(real))
|
|
19202
|
-
return;
|
|
19601
|
+
return 0;
|
|
19203
19602
|
if (!isContained(real, rootReal))
|
|
19204
|
-
return;
|
|
19603
|
+
return 0;
|
|
19205
19604
|
visited.add(real);
|
|
19605
|
+
let st;
|
|
19606
|
+
try {
|
|
19607
|
+
st = await (0, import_promises9.stat)(currentPath);
|
|
19608
|
+
} catch {
|
|
19609
|
+
return 0;
|
|
19610
|
+
}
|
|
19611
|
+
let subtreeMax = st.mtimeMs;
|
|
19612
|
+
if (!st.isDirectory())
|
|
19613
|
+
return subtreeMax;
|
|
19206
19614
|
let items;
|
|
19207
19615
|
try {
|
|
19208
|
-
items = await (0,
|
|
19616
|
+
items = await (0, import_promises9.readdir)(currentPath);
|
|
19209
19617
|
} catch {
|
|
19210
|
-
return;
|
|
19618
|
+
return subtreeMax;
|
|
19211
19619
|
}
|
|
19212
19620
|
items.sort();
|
|
19621
|
+
let entry;
|
|
19213
19622
|
if (items.includes("SKILL.md")) {
|
|
19214
19623
|
try {
|
|
19215
|
-
|
|
19216
|
-
|
|
19217
|
-
found.push(
|
|
19624
|
+
if ((await (0, import_promises9.stat)((0, import_node_path20.join)(currentPath, "SKILL.md"))).isFile()) {
|
|
19625
|
+
entry = { folder: currentPath, newestMtimeMs: 0 };
|
|
19626
|
+
found.push(entry);
|
|
19627
|
+
}
|
|
19218
19628
|
} catch {
|
|
19219
19629
|
}
|
|
19220
19630
|
}
|
|
19221
19631
|
for (const item of items) {
|
|
19222
19632
|
if (SKIP_DIRS.has(item))
|
|
19223
19633
|
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
|
-
}
|
|
19634
|
+
const childMax = await walk((0, import_node_path20.join)(currentPath, item));
|
|
19635
|
+
if (childMax > subtreeMax)
|
|
19636
|
+
subtreeMax = childMax;
|
|
19234
19637
|
}
|
|
19638
|
+
if (entry)
|
|
19639
|
+
entry.newestMtimeMs = subtreeMax;
|
|
19640
|
+
return subtreeMax;
|
|
19235
19641
|
}
|
|
19236
|
-
await walk((0,
|
|
19642
|
+
await walk((0, import_node_path20.resolve)(rootDir));
|
|
19237
19643
|
return found;
|
|
19238
19644
|
}
|
|
19239
|
-
|
|
19240
|
-
const
|
|
19645
|
+
function looseSkillKey(skillsRoot, folder, tag, scope) {
|
|
19646
|
+
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);
|
|
19647
|
+
return `skill:${tag}/${rel}@${scope}`;
|
|
19648
|
+
}
|
|
19649
|
+
async function discoverLooseSkills(skillsDir, tag, scope) {
|
|
19650
|
+
const resolvedRoot = (0, import_node_path20.resolve)(skillsDir);
|
|
19651
|
+
const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0, import_node_path20.resolve)(p.folder) !== resolvedRoot);
|
|
19652
|
+
return packages.map(({ folder, newestMtimeMs }) => ({
|
|
19653
|
+
key: looseSkillKey(resolvedRoot, folder, tag, scope),
|
|
19654
|
+
installPath: folder,
|
|
19655
|
+
version: scope,
|
|
19656
|
+
lastUpdated: new Date(newestMtimeMs).toISOString()
|
|
19657
|
+
}));
|
|
19658
|
+
}
|
|
19659
|
+
async function dedupeSkillsByResolvedPath(skills) {
|
|
19660
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19661
|
+
const out = [];
|
|
19662
|
+
for (const skill of skills) {
|
|
19663
|
+
let canonical = skill.installPath;
|
|
19664
|
+
try {
|
|
19665
|
+
canonical = await (0, import_promises9.realpath)(skill.installPath);
|
|
19666
|
+
} catch {
|
|
19667
|
+
}
|
|
19668
|
+
if (seen.has(canonical))
|
|
19669
|
+
continue;
|
|
19670
|
+
seen.add(canonical);
|
|
19671
|
+
out.push(skill);
|
|
19672
|
+
}
|
|
19673
|
+
return out;
|
|
19674
|
+
}
|
|
19675
|
+
async function discoverLooseSkillsAcrossRoots(roots) {
|
|
19676
|
+
const lists = await Promise.all(roots.map(async ({ dir, tag, scope }) => {
|
|
19677
|
+
try {
|
|
19678
|
+
return await discoverLooseSkills(dir, tag, scope);
|
|
19679
|
+
} catch {
|
|
19680
|
+
return [];
|
|
19681
|
+
}
|
|
19682
|
+
}));
|
|
19683
|
+
return lists.length <= 1 ? lists[0] ?? [] : dedupeSkillsByResolvedPath(lists.flat());
|
|
19684
|
+
}
|
|
19685
|
+
async function computeSkillIdsForRoot(rootDir, logger) {
|
|
19686
|
+
const folders = (await findSkillPackagesWithMtime(rootDir)).map((p) => p.folder);
|
|
19241
19687
|
const out = [];
|
|
19242
19688
|
for (const folder of folders) {
|
|
19243
19689
|
try {
|
|
19244
|
-
const entries = await entriesFromDirectory(folder);
|
|
19690
|
+
const entries = await entriesFromDirectory(folder, MAX_SKILL_BYTES);
|
|
19245
19691
|
const { skillId } = computeSkillId(entries);
|
|
19246
19692
|
out.push({ folder, skillId });
|
|
19247
|
-
} catch {
|
|
19693
|
+
} catch (e) {
|
|
19694
|
+
if (e instanceof SkillTooLargeError) {
|
|
19695
|
+
logger?.warn("Skill directory exceeds 50 MB limit; skipping", { folder });
|
|
19696
|
+
}
|
|
19248
19697
|
}
|
|
19249
19698
|
}
|
|
19250
19699
|
return out;
|
|
19251
19700
|
}
|
|
19252
19701
|
|
|
19702
|
+
// ../core/dist/skill-pending.js
|
|
19703
|
+
var import_promises10 = require("node:fs/promises");
|
|
19704
|
+
var import_node_path21 = require("node:path");
|
|
19705
|
+
init_config();
|
|
19706
|
+
init_file_utils();
|
|
19707
|
+
init_types2();
|
|
19708
|
+
var SCHEMA_VERSION2 = 1;
|
|
19709
|
+
var PENDING_TTL_MS = 60 * 60 * 1e3;
|
|
19710
|
+
function defaultPendingPath() {
|
|
19711
|
+
return (0, import_node_path21.join)(resolvePath(SAGE_DIR), "skill_pending.json");
|
|
19712
|
+
}
|
|
19713
|
+
async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19714
|
+
let raw;
|
|
19715
|
+
try {
|
|
19716
|
+
raw = await getFileContent(pendingPath);
|
|
19717
|
+
} catch {
|
|
19718
|
+
return { entries: {} };
|
|
19719
|
+
}
|
|
19720
|
+
try {
|
|
19721
|
+
const data = JSON.parse(raw);
|
|
19722
|
+
const rawEntries = data.entries ?? {};
|
|
19723
|
+
const now = Date.now();
|
|
19724
|
+
const entries = {};
|
|
19725
|
+
for (const [skillId, entry] of Object.entries(rawEntries)) {
|
|
19726
|
+
const folder = entry.folder;
|
|
19727
|
+
const submittedAt = entry.submitted_at;
|
|
19728
|
+
if (!folder || !submittedAt)
|
|
19729
|
+
continue;
|
|
19730
|
+
const ts = Date.parse(submittedAt);
|
|
19731
|
+
if (!isFreshTimestamp(ts, PENDING_TTL_MS, now))
|
|
19732
|
+
continue;
|
|
19733
|
+
entries[skillId] = {
|
|
19734
|
+
folder,
|
|
19735
|
+
submittedAt,
|
|
19736
|
+
agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
|
|
19737
|
+
containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
|
|
19738
|
+
};
|
|
19739
|
+
}
|
|
19740
|
+
return { entries };
|
|
19741
|
+
} catch (e) {
|
|
19742
|
+
logger.warn(`Failed to load pending marker from ${pendingPath}`, { error: String(e) });
|
|
19743
|
+
return { entries: {} };
|
|
19744
|
+
}
|
|
19745
|
+
}
|
|
19746
|
+
async function savePendingMarker(marker, pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19747
|
+
if (Object.keys(marker.entries).length === 0) {
|
|
19748
|
+
try {
|
|
19749
|
+
await (0, import_promises10.unlink)(pendingPath);
|
|
19750
|
+
} catch (e) {
|
|
19751
|
+
if (!e || typeof e !== "object" || e.code !== "ENOENT") {
|
|
19752
|
+
logger.warn(`Failed to delete empty pending marker at ${pendingPath}`, {
|
|
19753
|
+
error: String(e)
|
|
19754
|
+
});
|
|
19755
|
+
}
|
|
19756
|
+
}
|
|
19757
|
+
return;
|
|
19758
|
+
}
|
|
19759
|
+
try {
|
|
19760
|
+
const data = {
|
|
19761
|
+
schema_version: SCHEMA_VERSION2,
|
|
19762
|
+
entries: Object.fromEntries(Object.entries(marker.entries).map(([skillId, entry]) => [
|
|
19763
|
+
skillId,
|
|
19764
|
+
{
|
|
19765
|
+
folder: entry.folder,
|
|
19766
|
+
submitted_at: entry.submittedAt,
|
|
19767
|
+
agent_runtime: entry.agentRuntime,
|
|
19768
|
+
container_key: entry.containerKey
|
|
19769
|
+
}
|
|
19770
|
+
]))
|
|
19771
|
+
};
|
|
19772
|
+
await atomicWriteJson(pendingPath, data);
|
|
19773
|
+
} catch (e) {
|
|
19774
|
+
logger.warn(`Failed to save pending marker to ${pendingPath}`, { error: String(e) });
|
|
19775
|
+
}
|
|
19776
|
+
}
|
|
19777
|
+
function isPending(marker, skillId) {
|
|
19778
|
+
return skillId in marker.entries;
|
|
19779
|
+
}
|
|
19780
|
+
function addPending(marker, skillId, folder, origin = {}) {
|
|
19781
|
+
marker.entries[skillId] = {
|
|
19782
|
+
folder,
|
|
19783
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19784
|
+
agentRuntime: origin.agentRuntime,
|
|
19785
|
+
containerKey: origin.containerKey
|
|
19786
|
+
};
|
|
19787
|
+
}
|
|
19788
|
+
function removePending(marker, skillId) {
|
|
19789
|
+
delete marker.entries[skillId];
|
|
19790
|
+
}
|
|
19791
|
+
function listPending(marker) {
|
|
19792
|
+
return Object.entries(marker.entries).map(([skillId, entry]) => ({
|
|
19793
|
+
skillId,
|
|
19794
|
+
folder: entry.folder,
|
|
19795
|
+
agentRuntime: entry.agentRuntime,
|
|
19796
|
+
containerKey: entry.containerKey
|
|
19797
|
+
}));
|
|
19798
|
+
}
|
|
19799
|
+
|
|
19800
|
+
// ../core/dist/skill-verdict-cache.js
|
|
19801
|
+
var import_node_path22 = require("node:path");
|
|
19802
|
+
init_config();
|
|
19803
|
+
init_file_utils();
|
|
19804
|
+
init_types2();
|
|
19805
|
+
var SCHEMA_VERSION3 = 1;
|
|
19806
|
+
var DEFAULT_VERDICT_TTL_MS = MS_PER_DAY;
|
|
19807
|
+
function parseSources(raw) {
|
|
19808
|
+
if (!Array.isArray(raw))
|
|
19809
|
+
return void 0;
|
|
19810
|
+
const sources = [];
|
|
19811
|
+
for (const item of raw) {
|
|
19812
|
+
if (!item || typeof item !== "object")
|
|
19813
|
+
continue;
|
|
19814
|
+
const entry = item;
|
|
19815
|
+
const source = {
|
|
19816
|
+
agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
|
|
19817
|
+
containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
|
|
19818
|
+
};
|
|
19819
|
+
if (source.agentRuntime || source.containerKey)
|
|
19820
|
+
sources.push(source);
|
|
19821
|
+
}
|
|
19822
|
+
return sources.length > 0 ? sources : void 0;
|
|
19823
|
+
}
|
|
19824
|
+
function mergeSources(existing, incoming) {
|
|
19825
|
+
const merged = [];
|
|
19826
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19827
|
+
for (const source of [...existing ?? [], ...incoming ?? []]) {
|
|
19828
|
+
const key = source.agentRuntime ?? "";
|
|
19829
|
+
if (seen.has(key))
|
|
19830
|
+
continue;
|
|
19831
|
+
seen.add(key);
|
|
19832
|
+
merged.push(source);
|
|
19833
|
+
}
|
|
19834
|
+
return merged.length > 0 ? merged : void 0;
|
|
19835
|
+
}
|
|
19836
|
+
function defaultCachePath2() {
|
|
19837
|
+
return (0, import_node_path22.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
|
|
19838
|
+
}
|
|
19839
|
+
async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger = nullLogger) {
|
|
19840
|
+
let raw;
|
|
19841
|
+
try {
|
|
19842
|
+
raw = await getFileContent(cachePath);
|
|
19843
|
+
} catch {
|
|
19844
|
+
return { entries: {} };
|
|
19845
|
+
}
|
|
19846
|
+
try {
|
|
19847
|
+
const data = JSON.parse(raw);
|
|
19848
|
+
const rawEntries = data.entries ?? {};
|
|
19849
|
+
const now = Date.now();
|
|
19850
|
+
const entries = {};
|
|
19851
|
+
let pruned = 0;
|
|
19852
|
+
for (const [skillId, entry] of Object.entries(rawEntries)) {
|
|
19853
|
+
const analyzedAt = entry.analyzed_at;
|
|
19854
|
+
if (!analyzedAt)
|
|
19855
|
+
continue;
|
|
19856
|
+
const ts = Date.parse(analyzedAt);
|
|
19857
|
+
if (!isFreshTimestamp(ts, ttlMs, now)) {
|
|
19858
|
+
pruned++;
|
|
19859
|
+
continue;
|
|
19860
|
+
}
|
|
19861
|
+
entries[skillId] = {
|
|
19862
|
+
verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
|
|
19863
|
+
summary: typeof entry.summary === "string" ? entry.summary : void 0,
|
|
19864
|
+
skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
|
|
19865
|
+
sources: parseSources(entry.sources),
|
|
19866
|
+
analyzedAt
|
|
19867
|
+
};
|
|
19868
|
+
}
|
|
19869
|
+
const cache2 = { entries };
|
|
19870
|
+
if (pruned > 0) {
|
|
19871
|
+
logger.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
|
|
19872
|
+
await saveSkillVerdictCache(cache2, cachePath, logger);
|
|
19873
|
+
}
|
|
19874
|
+
return cache2;
|
|
19875
|
+
} catch (e) {
|
|
19876
|
+
logger.warn(`Failed to load verdict cache from ${cachePath}`, { error: String(e) });
|
|
19877
|
+
return { entries: {} };
|
|
19878
|
+
}
|
|
19879
|
+
}
|
|
19880
|
+
async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger = nullLogger) {
|
|
19881
|
+
try {
|
|
19882
|
+
const data = {
|
|
19883
|
+
schema_version: SCHEMA_VERSION3,
|
|
19884
|
+
entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
|
|
19885
|
+
skillId,
|
|
19886
|
+
{
|
|
19887
|
+
verdict: v.verdict,
|
|
19888
|
+
summary: v.summary,
|
|
19889
|
+
skill_name: v.skillName,
|
|
19890
|
+
sources: v.sources?.map((s) => ({
|
|
19891
|
+
agent_runtime: s.agentRuntime,
|
|
19892
|
+
container_key: s.containerKey
|
|
19893
|
+
})),
|
|
19894
|
+
analyzed_at: v.analyzedAt
|
|
19895
|
+
}
|
|
19896
|
+
]))
|
|
19897
|
+
};
|
|
19898
|
+
await atomicWriteJson(cachePath, data);
|
|
19899
|
+
} catch (e) {
|
|
19900
|
+
logger.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
|
|
19901
|
+
}
|
|
19902
|
+
}
|
|
19903
|
+
function getVerdict(cache2, skillId, ttlMs = DEFAULT_VERDICT_TTL_MS) {
|
|
19904
|
+
const entry = cache2.entries[skillId];
|
|
19905
|
+
if (!entry)
|
|
19906
|
+
return null;
|
|
19907
|
+
const ts = Date.parse(entry.analyzedAt);
|
|
19908
|
+
if (!isFreshTimestamp(ts, ttlMs))
|
|
19909
|
+
return null;
|
|
19910
|
+
return entry;
|
|
19911
|
+
}
|
|
19912
|
+
function putVerdict(cache2, skillId, verdict) {
|
|
19913
|
+
const existing = cache2.entries[skillId];
|
|
19914
|
+
cache2.entries[skillId] = {
|
|
19915
|
+
...verdict,
|
|
19916
|
+
sources: mergeSources(existing?.sources, verdict.sources),
|
|
19917
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19918
|
+
};
|
|
19919
|
+
}
|
|
19920
|
+
|
|
19253
19921
|
// ../core/dist/plugin-scanner.js
|
|
19254
19922
|
init_types2();
|
|
19255
19923
|
var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
@@ -19274,15 +19942,34 @@ var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
|
19274
19942
|
var MAX_FILE_SIZE = 512 * 1024;
|
|
19275
19943
|
async function walkPluginFiles(installPath, logger) {
|
|
19276
19944
|
const files = [];
|
|
19945
|
+
const visited = /* @__PURE__ */ new Set();
|
|
19946
|
+
let rootReal;
|
|
19947
|
+
try {
|
|
19948
|
+
rootReal = await (0, import_promises11.realpath)((0, import_node_path23.resolve)(installPath));
|
|
19949
|
+
} catch (e) {
|
|
19950
|
+
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
19951
|
+
return files;
|
|
19952
|
+
}
|
|
19277
19953
|
async function walk(dirOrFile) {
|
|
19954
|
+
let real;
|
|
19955
|
+
try {
|
|
19956
|
+
real = await (0, import_promises11.realpath)(dirOrFile);
|
|
19957
|
+
} catch {
|
|
19958
|
+
return;
|
|
19959
|
+
}
|
|
19960
|
+
if (visited.has(real))
|
|
19961
|
+
return;
|
|
19962
|
+
if (!isContained(real, rootReal))
|
|
19963
|
+
return;
|
|
19964
|
+
visited.add(real);
|
|
19278
19965
|
let stats;
|
|
19279
19966
|
try {
|
|
19280
|
-
stats = await (0,
|
|
19967
|
+
stats = await (0, import_promises11.stat)(dirOrFile);
|
|
19281
19968
|
} catch {
|
|
19282
19969
|
return;
|
|
19283
19970
|
}
|
|
19284
19971
|
if (stats.isFile()) {
|
|
19285
|
-
if (SCANNABLE_EXTENSIONS2.has((0,
|
|
19972
|
+
if (SCANNABLE_EXTENSIONS2.has((0, import_node_path23.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
|
|
19286
19973
|
files.push(dirOrFile);
|
|
19287
19974
|
}
|
|
19288
19975
|
return;
|
|
@@ -19290,32 +19977,41 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
19290
19977
|
if (stats.isDirectory()) {
|
|
19291
19978
|
let entries;
|
|
19292
19979
|
try {
|
|
19293
|
-
entries = await (0,
|
|
19980
|
+
entries = await (0, import_promises11.readdir)(dirOrFile);
|
|
19294
19981
|
} catch {
|
|
19295
19982
|
return;
|
|
19296
19983
|
}
|
|
19297
19984
|
for (const entry of entries) {
|
|
19298
19985
|
if (SKIP_DIRS.has(entry))
|
|
19299
19986
|
continue;
|
|
19300
|
-
const fullPath = (0,
|
|
19987
|
+
const fullPath = (0, import_node_path23.join)(dirOrFile, entry);
|
|
19301
19988
|
await walk(fullPath);
|
|
19302
19989
|
}
|
|
19303
19990
|
}
|
|
19304
19991
|
}
|
|
19305
19992
|
try {
|
|
19306
|
-
await walk(installPath);
|
|
19993
|
+
await walk((0, import_node_path23.resolve)(installPath));
|
|
19307
19994
|
} catch (e) {
|
|
19308
19995
|
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
19309
19996
|
}
|
|
19310
19997
|
return files;
|
|
19311
19998
|
}
|
|
19312
19999
|
async function scanPlugin(plugin, options = {}) {
|
|
19313
|
-
const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger } = options;
|
|
20000
|
+
const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger, skillVerdictCachePath, skillPendingPath, loggingConfig, skillVerdictTtlMs, uploadEnabled = true, deferUnknownSkills = false, agentRuntime } = options;
|
|
19314
20001
|
const result = { plugin, findings: [] };
|
|
19315
|
-
const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger
|
|
20002
|
+
const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger, {
|
|
20003
|
+
verdictCachePath: skillVerdictCachePath,
|
|
20004
|
+
pendingPath: skillPendingPath,
|
|
20005
|
+
loggingConfig,
|
|
20006
|
+
verdictTtlMs: skillVerdictTtlMs,
|
|
20007
|
+
uploadEnabled,
|
|
20008
|
+
deferUnknownSkills,
|
|
20009
|
+
agentRuntime
|
|
20010
|
+
}) : Promise.resolve(false);
|
|
19316
20011
|
const files = await walkPluginFiles(plugin.installPath, logger);
|
|
19317
20012
|
if (files.length === 0) {
|
|
19318
|
-
await skillCheckPromise
|
|
20013
|
+
if (await skillCheckPromise)
|
|
20014
|
+
result.deferCache = true;
|
|
19319
20015
|
return result;
|
|
19320
20016
|
}
|
|
19321
20017
|
const allUrls = [];
|
|
@@ -19331,7 +20027,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19331
20027
|
}
|
|
19332
20028
|
if (amsiClient) {
|
|
19333
20029
|
try {
|
|
19334
|
-
const scanName = `${plugin.key}/${(0,
|
|
20030
|
+
const scanName = `${plugin.key}/${(0, import_node_path23.relative)(plugin.installPath, filePath)}`;
|
|
19335
20031
|
const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
|
|
19336
20032
|
if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
|
|
19337
20033
|
result.findings.push({
|
|
@@ -19339,7 +20035,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19339
20035
|
title: `AMSI detection (result=${amsiResult.amsiResult})`,
|
|
19340
20036
|
severity: "critical",
|
|
19341
20037
|
artifact: content.slice(0, 200),
|
|
19342
|
-
sourceFile: (0,
|
|
20038
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
|
|
19343
20039
|
});
|
|
19344
20040
|
}
|
|
19345
20041
|
} catch {
|
|
@@ -19392,7 +20088,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19392
20088
|
title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
|
|
19393
20089
|
severity: "critical",
|
|
19394
20090
|
artifact: fr.sha256,
|
|
19395
|
-
sourceFile: (0,
|
|
20091
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
|
|
19396
20092
|
});
|
|
19397
20093
|
}
|
|
19398
20094
|
}
|
|
@@ -19400,46 +20096,109 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19400
20096
|
} catch {
|
|
19401
20097
|
}
|
|
19402
20098
|
})() : Promise.resolve();
|
|
19403
|
-
await Promise.all([
|
|
20099
|
+
const [, , skillDeferCache] = await Promise.all([
|
|
20100
|
+
urlCheckPromise,
|
|
20101
|
+
fileCheckPromise,
|
|
20102
|
+
skillCheckPromise
|
|
20103
|
+
]);
|
|
20104
|
+
if (skillDeferCache)
|
|
20105
|
+
result.deferCache = true;
|
|
19404
20106
|
return result;
|
|
19405
20107
|
}
|
|
19406
|
-
|
|
20108
|
+
function pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict) {
|
|
20109
|
+
const risk = (verdict.verdict ?? "").toUpperCase();
|
|
20110
|
+
if (risk !== "HIGH" && risk !== "CRITICAL")
|
|
20111
|
+
return;
|
|
20112
|
+
const severity = risk === "CRITICAL" ? "critical" : "warning";
|
|
20113
|
+
findings.push({
|
|
20114
|
+
threatId: "SKILL_CHECK",
|
|
20115
|
+
title: verdict.summary?.trim() || (risk === "CRITICAL" ? "Malicious skill detected" : "Suspicious skill detected"),
|
|
20116
|
+
severity,
|
|
20117
|
+
artifact: skillId.slice(0, 16),
|
|
20118
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, folder) || "."
|
|
20119
|
+
});
|
|
20120
|
+
}
|
|
20121
|
+
async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
19407
20122
|
try {
|
|
19408
|
-
const skills = await computeSkillIdsForRoot(plugin.installPath);
|
|
20123
|
+
const skills = await computeSkillIdsForRoot(plugin.installPath, logger);
|
|
19409
20124
|
if (skills.length === 0)
|
|
19410
|
-
return;
|
|
19411
|
-
const
|
|
20125
|
+
return false;
|
|
20126
|
+
const verdictCache = await loadSkillVerdictCache(paths.verdictCachePath, paths.verdictTtlMs, logger);
|
|
20127
|
+
const uncached = [];
|
|
20128
|
+
for (const skill of skills) {
|
|
20129
|
+
const cached = getVerdict(verdictCache, skill.skillId, paths.verdictTtlMs);
|
|
20130
|
+
if (cached) {
|
|
20131
|
+
pushSkillFindingIfRisky(findings, plugin, skill.skillId, skill.folder, cached);
|
|
20132
|
+
} else {
|
|
20133
|
+
uncached.push(skill);
|
|
20134
|
+
}
|
|
20135
|
+
}
|
|
20136
|
+
if (uncached.length === 0)
|
|
20137
|
+
return false;
|
|
19412
20138
|
const client = new SkillCheckClient(void 0, logger);
|
|
19413
|
-
const verdicts = await client.checkSkills(
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
|
|
20139
|
+
const verdicts = await client.checkSkills(uncached.map((s) => s.skillId));
|
|
20140
|
+
const marker = await loadPendingMarker(paths.pendingPath, logger);
|
|
20141
|
+
let cacheModified = false;
|
|
20142
|
+
let markerModified = false;
|
|
20143
|
+
let lookupFailed = false;
|
|
20144
|
+
let graceDeferred = false;
|
|
20145
|
+
for (const { folder, skillId } of uncached) {
|
|
20146
|
+
if (!verdicts.has(skillId)) {
|
|
20147
|
+
lookupFailed = true;
|
|
19420
20148
|
continue;
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
|
|
20149
|
+
}
|
|
20150
|
+
const verdict = verdicts.get(skillId) ?? null;
|
|
20151
|
+
if (verdict) {
|
|
20152
|
+
putVerdict(verdictCache, skillId, {
|
|
20153
|
+
verdict: verdict.verdict,
|
|
20154
|
+
summary: verdict.summary,
|
|
20155
|
+
skillName: (0, import_node_path23.basename)(folder),
|
|
20156
|
+
sources: [{ agentRuntime: paths.agentRuntime, containerKey: plugin.key }]
|
|
20157
|
+
});
|
|
20158
|
+
cacheModified = true;
|
|
20159
|
+
pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict);
|
|
20160
|
+
if (isPending(marker, skillId)) {
|
|
20161
|
+
removePending(marker, skillId);
|
|
20162
|
+
markerModified = true;
|
|
20163
|
+
}
|
|
20164
|
+
} else if (!isPending(marker, skillId)) {
|
|
20165
|
+
if (paths.uploadEnabled === false) {
|
|
20166
|
+
if (paths.deferUnknownSkills)
|
|
20167
|
+
graceDeferred = true;
|
|
20168
|
+
} else {
|
|
20169
|
+
addPending(marker, skillId, folder, {
|
|
20170
|
+
agentRuntime: paths.agentRuntime,
|
|
20171
|
+
containerKey: plugin.key
|
|
20172
|
+
});
|
|
20173
|
+
markerModified = true;
|
|
20174
|
+
if (paths.loggingConfig) {
|
|
20175
|
+
logSkillQueued(paths.loggingConfig, skillId, plugin.key, folder).catch(() => {
|
|
20176
|
+
});
|
|
20177
|
+
}
|
|
20178
|
+
}
|
|
20179
|
+
}
|
|
19430
20180
|
}
|
|
20181
|
+
if (cacheModified)
|
|
20182
|
+
await saveSkillVerdictCache(verdictCache, paths.verdictCachePath, logger);
|
|
20183
|
+
if (markerModified)
|
|
20184
|
+
await savePendingMarker(marker, paths.pendingPath, logger);
|
|
20185
|
+
const anyStillPending = uncached.some(({ skillId }) => isPending(marker, skillId));
|
|
20186
|
+
return markerModified || anyStillPending || lookupFailed || graceDeferred;
|
|
19431
20187
|
} catch (e) {
|
|
19432
20188
|
logger.debug("Skill check failed", { error: String(e) });
|
|
20189
|
+
return false;
|
|
19433
20190
|
}
|
|
19434
20191
|
}
|
|
19435
20192
|
|
|
19436
20193
|
// ../core/dist/session-start.js
|
|
19437
20194
|
var import_node_child_process2 = require("node:child_process");
|
|
19438
20195
|
var import_node_fs4 = require("node:fs");
|
|
20196
|
+
var import_node_path25 = require("node:path");
|
|
19439
20197
|
init_config();
|
|
19440
20198
|
init_file_utils();
|
|
19441
20199
|
|
|
19442
20200
|
// ../core/dist/session-start-scan.js
|
|
20201
|
+
var import_node_path24 = require("node:path");
|
|
19443
20202
|
init_config();
|
|
19444
20203
|
init_types2();
|
|
19445
20204
|
function fromCachedFinding(finding) {
|
|
@@ -19522,6 +20281,9 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19522
20281
|
deniedByException: 0,
|
|
19523
20282
|
findingsCount: 0
|
|
19524
20283
|
};
|
|
20284
|
+
const skillPendingPath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_pending.json") : void 0;
|
|
20285
|
+
const skillVerdictCachePath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_verdict_cache.json") : void 0;
|
|
20286
|
+
const cacheTtlMs = skillCacheTtlMs(config2);
|
|
19525
20287
|
for (const plugin of plugins) {
|
|
19526
20288
|
const denyMatch = findPluginDenyException(exceptions, plugin.key);
|
|
19527
20289
|
if (denyMatch) {
|
|
@@ -19546,7 +20308,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19546
20308
|
stats.allowedByException += 1;
|
|
19547
20309
|
continue;
|
|
19548
20310
|
}
|
|
19549
|
-
const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
|
|
20311
|
+
const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated, cacheTtlMs);
|
|
19550
20312
|
if (cached && cached.findings.length === 0) {
|
|
19551
20313
|
stats.cacheHitsClean += 1;
|
|
19552
20314
|
continue;
|
|
@@ -19565,11 +20327,23 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19565
20327
|
const result = await scanPlugin(plugin, {
|
|
19566
20328
|
checkUrls: context.checkUrls ?? true,
|
|
19567
20329
|
checkFileHashes: context.checkFileHashes ?? true,
|
|
20330
|
+
checkSkills: config2.skill_check.enabled,
|
|
20331
|
+
uploadEnabled: context.uploadUnknownSkills ?? config2.skill_check.upload_enabled,
|
|
20332
|
+
deferUnknownSkills: context.deferUnknownSkills,
|
|
19568
20333
|
amsiClient,
|
|
19569
|
-
logger
|
|
20334
|
+
logger,
|
|
20335
|
+
skillPendingPath,
|
|
20336
|
+
skillVerdictCachePath,
|
|
20337
|
+
skillVerdictTtlMs: cacheTtlMs,
|
|
20338
|
+
loggingConfig: config2.logging,
|
|
20339
|
+
agentRuntime: context.agentRuntime
|
|
19570
20340
|
});
|
|
19571
|
-
|
|
19572
|
-
|
|
20341
|
+
if (result.deferCache) {
|
|
20342
|
+
logger.debug("Plugin cache deferred (skill verdict pending)", { plugin: plugin.key });
|
|
20343
|
+
} else {
|
|
20344
|
+
storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
|
|
20345
|
+
cacheModified = true;
|
|
20346
|
+
}
|
|
19573
20347
|
if (result.findings.length > 0) {
|
|
19574
20348
|
stats.findingsCount += result.findings.length;
|
|
19575
20349
|
resultsWithFindings.push(result);
|
|
@@ -19596,6 +20370,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19596
20370
|
init_types2();
|
|
19597
20371
|
|
|
19598
20372
|
// ../core/dist/version-check.js
|
|
20373
|
+
init_config();
|
|
19599
20374
|
init_types2();
|
|
19600
20375
|
var DEFAULT_TIMEOUT_MS3 = 5e3;
|
|
19601
20376
|
function isNewerVersion(current, latest) {
|
|
@@ -19622,11 +20397,20 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
|
|
|
19622
20397
|
logger.debug("Skipping version check: missing installation id");
|
|
19623
20398
|
return null;
|
|
19624
20399
|
}
|
|
20400
|
+
let config2 = context.config;
|
|
20401
|
+
if (!config2) {
|
|
20402
|
+
try {
|
|
20403
|
+
config2 = await loadConfig(context.configPath, logger);
|
|
20404
|
+
} catch (err) {
|
|
20405
|
+
logger.debug(`Version check config load failed: ${err}`);
|
|
20406
|
+
}
|
|
20407
|
+
}
|
|
19625
20408
|
const envelope = buildSageProxyEnvelope({
|
|
19626
20409
|
iid: context.iid,
|
|
19627
20410
|
versionApp: currentVersion,
|
|
19628
20411
|
agentRuntime: context.agentRuntime,
|
|
19629
|
-
agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown"
|
|
20412
|
+
agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown",
|
|
20413
|
+
config: config2
|
|
19630
20414
|
});
|
|
19631
20415
|
const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
|
|
19632
20416
|
const enriched = mergeExtendedInfo(envelope, extendedInfo);
|
|
@@ -19666,6 +20450,8 @@ async function runSessionStart(ctx) {
|
|
|
19666
20450
|
const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
|
|
19667
20451
|
pruneOrphanedTmpFiles(sageDirPath).catch(() => {
|
|
19668
20452
|
});
|
|
20453
|
+
deployConfigDefaults(sageDirPath, logger).catch(() => {
|
|
20454
|
+
});
|
|
19669
20455
|
maybeSpawnModelDownloadWorker({
|
|
19670
20456
|
sageDirPath,
|
|
19671
20457
|
workerPath: ctx.modelDownloadWorkerPath,
|
|
@@ -19676,6 +20462,13 @@ async function runSessionStart(ctx) {
|
|
|
19676
20462
|
logger
|
|
19677
20463
|
}).catch(() => {
|
|
19678
20464
|
});
|
|
20465
|
+
const rollout = await resolveSkillUploadRollout({
|
|
20466
|
+
sageDirPath,
|
|
20467
|
+
configPath: ctx.configPath,
|
|
20468
|
+
version: ctx.version,
|
|
20469
|
+
logger,
|
|
20470
|
+
commitNotice: ctx.commitUploadNotice ?? true
|
|
20471
|
+
});
|
|
19679
20472
|
const iidPromise = getInstallationId(sageDirPath).catch(() => void 0);
|
|
19680
20473
|
const [scanResults, versionCheck] = await Promise.all([
|
|
19681
20474
|
runSessionStartScan({
|
|
@@ -19687,7 +20480,11 @@ async function runSessionStart(ctx) {
|
|
|
19687
20480
|
configPath: ctx.configPath,
|
|
19688
20481
|
scanCachePath: ctx.scanCachePath,
|
|
19689
20482
|
checkUrls: ctx.checkUrls,
|
|
19690
|
-
checkFileHashes: ctx.checkFileHashes
|
|
20483
|
+
checkFileHashes: ctx.checkFileHashes,
|
|
20484
|
+
sageDirPath,
|
|
20485
|
+
agentRuntime: ctx.agentRuntime,
|
|
20486
|
+
uploadUnknownSkills: rollout.uploadActive,
|
|
20487
|
+
deferUnknownSkills: rollout.deferUnknownSkills
|
|
19691
20488
|
}),
|
|
19692
20489
|
iidPromise.then((iid) => {
|
|
19693
20490
|
if (!iid) {
|
|
@@ -19696,11 +20493,25 @@ async function runSessionStart(ctx) {
|
|
|
19696
20493
|
return checkForUpdate(ctx.version, logger, void 0, {
|
|
19697
20494
|
agentRuntime: ctx.agentRuntime,
|
|
19698
20495
|
agentRuntimeVersion: ctx.agentRuntimeVersion,
|
|
19699
|
-
iid
|
|
20496
|
+
iid,
|
|
20497
|
+
configPath: ctx.configPath
|
|
19700
20498
|
});
|
|
19701
20499
|
}).catch(() => null)
|
|
19702
20500
|
]);
|
|
19703
|
-
|
|
20501
|
+
maybeSpawnSkillUploadWorker({
|
|
20502
|
+
sageDirPath,
|
|
20503
|
+
workerPath: ctx.skillUploadWorkerPath,
|
|
20504
|
+
configPath: ctx.configPath,
|
|
20505
|
+
agentRuntime: ctx.agentRuntime,
|
|
20506
|
+
uploadActive: rollout.uploadActive,
|
|
20507
|
+
logger
|
|
20508
|
+
}).catch(() => {
|
|
20509
|
+
});
|
|
20510
|
+
return {
|
|
20511
|
+
scanResults,
|
|
20512
|
+
versionCheck,
|
|
20513
|
+
notices: rollout.showNotice ? [SKILL_UPLOAD_NOTICE.id] : []
|
|
20514
|
+
};
|
|
19704
20515
|
}
|
|
19705
20516
|
async function maybeSpawnModelDownloadWorker(args) {
|
|
19706
20517
|
if (!args.workerPath) {
|
|
@@ -19746,6 +20557,7 @@ async function maybeSpawnModelDownloadWorker(args) {
|
|
|
19746
20557
|
const spawned = spawnModelDownloadWorker({
|
|
19747
20558
|
sageDirPath: args.sageDirPath,
|
|
19748
20559
|
workerPath: args.workerPath,
|
|
20560
|
+
configPath: args.configPath,
|
|
19749
20561
|
agentRuntime: args.agentRuntime,
|
|
19750
20562
|
agentRuntimeVersion: args.agentRuntimeVersion,
|
|
19751
20563
|
versionApp: args.versionApp,
|
|
@@ -19757,6 +20569,9 @@ async function maybeSpawnModelDownloadWorker(args) {
|
|
|
19757
20569
|
missingModels
|
|
19758
20570
|
});
|
|
19759
20571
|
}
|
|
20572
|
+
function bunRuntimeEnv() {
|
|
20573
|
+
return process.versions.bun ? { BUN_BE_BUN: "1" } : {};
|
|
20574
|
+
}
|
|
19760
20575
|
function spawnModelDownloadWorker(args) {
|
|
19761
20576
|
const logger = args.logger ?? nullLogger;
|
|
19762
20577
|
try {
|
|
@@ -19765,7 +20580,9 @@ function spawnModelDownloadWorker(args) {
|
|
|
19765
20580
|
stdio: "ignore",
|
|
19766
20581
|
env: {
|
|
19767
20582
|
...process.env,
|
|
20583
|
+
...bunRuntimeEnv(),
|
|
19768
20584
|
SAGE_DIR: args.sageDirPath,
|
|
20585
|
+
SAGE_CONFIG_PATH: args.configPath ?? "",
|
|
19769
20586
|
SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown"),
|
|
19770
20587
|
SAGE_AGENT_RUNTIME_VERSION: args.agentRuntimeVersion ?? "",
|
|
19771
20588
|
SAGE_VERSION_APP: args.versionApp ?? "",
|
|
@@ -19779,9 +20596,83 @@ function spawnModelDownloadWorker(args) {
|
|
|
19779
20596
|
return false;
|
|
19780
20597
|
}
|
|
19781
20598
|
}
|
|
20599
|
+
async function maybeSpawnSkillUploadWorker(args) {
|
|
20600
|
+
if (!args.uploadActive) {
|
|
20601
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20602
|
+
result: "skipped",
|
|
20603
|
+
skippedReason: "upload_inactive"
|
|
20604
|
+
});
|
|
20605
|
+
return;
|
|
20606
|
+
}
|
|
20607
|
+
try {
|
|
20608
|
+
const config2 = await loadConfig(args.configPath, args.logger);
|
|
20609
|
+
if (!config2.skill_check.enabled) {
|
|
20610
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20611
|
+
result: "skipped",
|
|
20612
|
+
skippedReason: "skill_check_disabled"
|
|
20613
|
+
});
|
|
20614
|
+
return;
|
|
20615
|
+
}
|
|
20616
|
+
} catch {
|
|
20617
|
+
}
|
|
20618
|
+
if (!args.workerPath) {
|
|
20619
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20620
|
+
result: "skipped",
|
|
20621
|
+
skippedReason: "missing_worker_path"
|
|
20622
|
+
});
|
|
20623
|
+
return;
|
|
20624
|
+
}
|
|
20625
|
+
if (!(0, import_node_fs4.existsSync)(args.workerPath)) {
|
|
20626
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20627
|
+
result: "skipped",
|
|
20628
|
+
skippedReason: "worker_script_not_found",
|
|
20629
|
+
workerPath: args.workerPath
|
|
20630
|
+
});
|
|
20631
|
+
return;
|
|
20632
|
+
}
|
|
20633
|
+
const marker = await loadPendingMarker((0, import_node_path25.join)(args.sageDirPath, "skill_pending.json"), args.logger);
|
|
20634
|
+
const pendingCount = listPending(marker).length;
|
|
20635
|
+
if (pendingCount === 0) {
|
|
20636
|
+
args.logger.debug("Skill upload worker skipped", {
|
|
20637
|
+
result: "skipped",
|
|
20638
|
+
skippedReason: "no_pending_skills"
|
|
20639
|
+
});
|
|
20640
|
+
return;
|
|
20641
|
+
}
|
|
20642
|
+
const spawned = spawnSkillUploadWorker({
|
|
20643
|
+
sageDirPath: args.sageDirPath,
|
|
20644
|
+
workerPath: args.workerPath,
|
|
20645
|
+
agentRuntime: args.agentRuntime,
|
|
20646
|
+
logger: args.logger
|
|
20647
|
+
});
|
|
20648
|
+
args.logger.debug("Skill upload worker spawn completed", {
|
|
20649
|
+
result: spawned ? "spawned" : "failed",
|
|
20650
|
+
pendingCount
|
|
20651
|
+
});
|
|
20652
|
+
}
|
|
20653
|
+
function spawnSkillUploadWorker(args) {
|
|
20654
|
+
const logger = args.logger ?? nullLogger;
|
|
20655
|
+
try {
|
|
20656
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [args.workerPath], {
|
|
20657
|
+
detached: true,
|
|
20658
|
+
stdio: "ignore",
|
|
20659
|
+
env: {
|
|
20660
|
+
...process.env,
|
|
20661
|
+
...bunRuntimeEnv(),
|
|
20662
|
+
SAGE_DIR: args.sageDirPath,
|
|
20663
|
+
SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown")
|
|
20664
|
+
}
|
|
20665
|
+
});
|
|
20666
|
+
child.unref();
|
|
20667
|
+
return true;
|
|
20668
|
+
} catch (err) {
|
|
20669
|
+
logger.warn("Failed to spawn skill upload worker", { error: String(err) });
|
|
20670
|
+
return false;
|
|
20671
|
+
}
|
|
20672
|
+
}
|
|
19782
20673
|
|
|
19783
20674
|
// ../core/dist/scan-handler.js
|
|
19784
|
-
async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion) {
|
|
20675
|
+
async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans = true) {
|
|
19785
20676
|
logger.debug(`${branding2.name} plugin scan started (${context})`, {
|
|
19786
20677
|
agentRuntime,
|
|
19787
20678
|
pluginsCount: plugins.length,
|
|
@@ -19796,8 +20687,20 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
|
|
|
19796
20687
|
logger,
|
|
19797
20688
|
agentRuntime,
|
|
19798
20689
|
agentRuntimeVersion,
|
|
19799
|
-
modelDownloadWorkerPath
|
|
20690
|
+
modelDownloadWorkerPath,
|
|
20691
|
+
skillUploadWorkerPath,
|
|
20692
|
+
commitUploadNotice
|
|
19800
20693
|
});
|
|
20694
|
+
if (result.notices && result.notices.length > 0) {
|
|
20695
|
+
logger.debug(`${branding2.name} session-start produced notices`, {
|
|
20696
|
+
context,
|
|
20697
|
+
noticeIds: result.notices,
|
|
20698
|
+
forwarded: Boolean(onNotices)
|
|
20699
|
+
});
|
|
20700
|
+
onNotices?.(result.notices);
|
|
20701
|
+
} else {
|
|
20702
|
+
logger.debug(`${branding2.name} session-start produced no notices`, { context });
|
|
20703
|
+
}
|
|
19801
20704
|
const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
|
|
19802
20705
|
const completionData = {
|
|
19803
20706
|
agentRuntime,
|
|
@@ -19811,10 +20714,13 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
|
|
|
19811
20714
|
} else {
|
|
19812
20715
|
logger.debug(`${branding2.name} plugin scan (${context}) complete`, completionData);
|
|
19813
20716
|
}
|
|
20717
|
+
if (findingsCount === 0 && !announceCleanScans) {
|
|
20718
|
+
return "";
|
|
20719
|
+
}
|
|
19814
20720
|
return formatSessionStartMessage(version, result, branding2, style);
|
|
19815
20721
|
}
|
|
19816
20722
|
function createScanHandler(options) {
|
|
19817
|
-
const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
|
|
20723
|
+
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
20724
|
return async () => {
|
|
19819
20725
|
try {
|
|
19820
20726
|
let plugins = await discoverPlugins2();
|
|
@@ -19822,8 +20728,10 @@ function createScanHandler(options) {
|
|
|
19822
20728
|
if (plugins.length === 0) {
|
|
19823
20729
|
logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
|
|
19824
20730
|
}
|
|
19825
|
-
const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion);
|
|
19826
|
-
|
|
20731
|
+
const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans);
|
|
20732
|
+
if (msg !== "") {
|
|
20733
|
+
onResult?.(msg);
|
|
20734
|
+
}
|
|
19827
20735
|
} catch (e) {
|
|
19828
20736
|
logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
|
|
19829
20737
|
}
|
|
@@ -19861,19 +20769,19 @@ function canonicalizeToolName(map, rawName) {
|
|
|
19861
20769
|
init_types2();
|
|
19862
20770
|
|
|
19863
20771
|
// src/bundled-dirs.ts
|
|
19864
|
-
var
|
|
19865
|
-
var packageRoot = (0,
|
|
20772
|
+
var import_node_path26 = require("node:path");
|
|
20773
|
+
var packageRoot = (0, import_node_path26.resolve)(__dirname, "..");
|
|
19866
20774
|
function getBundledDataDirs() {
|
|
19867
20775
|
return {
|
|
19868
|
-
threatsDir: (0,
|
|
19869
|
-
trustedDomainsDir: (0,
|
|
20776
|
+
threatsDir: (0, import_node_path26.join)(packageRoot, "resources", "threats"),
|
|
20777
|
+
trustedDomainsDir: (0, import_node_path26.join)(packageRoot, "resources", "trusted-domains")
|
|
19870
20778
|
};
|
|
19871
20779
|
}
|
|
19872
20780
|
var cachedVersion;
|
|
19873
20781
|
function getSageVersion() {
|
|
19874
20782
|
if (cachedVersion) return cachedVersion;
|
|
19875
20783
|
try {
|
|
19876
|
-
const pkg = JSON.parse(getFileContentSync((0,
|
|
20784
|
+
const pkg = JSON.parse(getFileContentSync((0, import_node_path26.join)(packageRoot, "package.json"), "utf-8"));
|
|
19877
20785
|
cachedVersion = pkg.version ?? "0.0.0";
|
|
19878
20786
|
} catch {
|
|
19879
20787
|
cachedVersion = "0.0.0";
|
|
@@ -19882,37 +20790,41 @@ function getSageVersion() {
|
|
|
19882
20790
|
}
|
|
19883
20791
|
|
|
19884
20792
|
// src/startup-scan.ts
|
|
19885
|
-
var
|
|
20793
|
+
var import_node_path28 = require("node:path");
|
|
19886
20794
|
|
|
19887
20795
|
// src/plugin-discovery.ts
|
|
19888
|
-
var
|
|
20796
|
+
var import_promises12 = require("node:fs/promises");
|
|
19889
20797
|
var import_node_os4 = require("node:os");
|
|
19890
|
-
var
|
|
19891
|
-
var DEFAULT_EXTENSIONS_DIR = (0,
|
|
19892
|
-
|
|
20798
|
+
var import_node_path27 = require("node:path");
|
|
20799
|
+
var DEFAULT_EXTENSIONS_DIR = (0, import_node_path27.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
|
|
20800
|
+
var SKILL_ROOTS = [
|
|
20801
|
+
{ dir: (0, import_node_path27.join)(getHomeDir(), ".agents", "skills"), tag: "agents", scope: "personal" },
|
|
20802
|
+
{ dir: (0, import_node_path27.join)(getHomeDir(), ".openclaw", "skills"), tag: "openclaw", scope: "personal" }
|
|
20803
|
+
];
|
|
20804
|
+
async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding, skillRoots = SKILL_ROOTS) {
|
|
19893
20805
|
logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
|
|
19894
20806
|
path: extensionsDir
|
|
19895
20807
|
});
|
|
20808
|
+
const plugins = [];
|
|
19896
20809
|
let entries;
|
|
19897
20810
|
try {
|
|
19898
|
-
entries = await (0,
|
|
20811
|
+
entries = await (0, import_promises12.readdir)(extensionsDir);
|
|
19899
20812
|
} catch {
|
|
19900
20813
|
logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
|
|
19901
|
-
|
|
20814
|
+
entries = [];
|
|
19902
20815
|
}
|
|
19903
|
-
const plugins = [];
|
|
19904
20816
|
for (const entry of entries) {
|
|
19905
|
-
const extDir = (0,
|
|
20817
|
+
const extDir = (0, import_node_path27.join)(extensionsDir, entry);
|
|
19906
20818
|
let stats;
|
|
19907
20819
|
try {
|
|
19908
|
-
stats = await (0,
|
|
20820
|
+
stats = await (0, import_promises12.stat)(extDir);
|
|
19909
20821
|
if (!stats.isDirectory()) continue;
|
|
19910
20822
|
} catch {
|
|
19911
20823
|
continue;
|
|
19912
20824
|
}
|
|
19913
20825
|
let pkg;
|
|
19914
20826
|
try {
|
|
19915
|
-
const pkgPath = (0,
|
|
20827
|
+
const pkgPath = (0, import_node_path27.join)(extDir, "package.json");
|
|
19916
20828
|
const raw = await getFileContent(pkgPath);
|
|
19917
20829
|
pkg = JSON.parse(raw);
|
|
19918
20830
|
} catch {
|
|
@@ -19934,11 +20846,14 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
19934
20846
|
});
|
|
19935
20847
|
}
|
|
19936
20848
|
logger.debug(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
|
|
20849
|
+
const skills = await discoverLooseSkillsAcrossRoots(skillRoots);
|
|
20850
|
+
logger.debug(`${branding2.name} skill discovery: found ${skills.length} loose skill(s)`);
|
|
20851
|
+
plugins.push(...skills);
|
|
19937
20852
|
return plugins;
|
|
19938
20853
|
}
|
|
19939
20854
|
|
|
19940
20855
|
// src/startup-scan.ts
|
|
19941
|
-
function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult) {
|
|
20856
|
+
function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult, announceCleanScans = true) {
|
|
19942
20857
|
const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
|
|
19943
20858
|
const version = getSageVersion();
|
|
19944
20859
|
return createScanHandler({
|
|
@@ -19953,29 +20868,58 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
|
|
|
19953
20868
|
// No agentRuntimeVersion: the OpenClaw plugin API exposes no host version.
|
|
19954
20869
|
branding: branding2,
|
|
19955
20870
|
onResult,
|
|
19956
|
-
|
|
20871
|
+
// No onNotices: OpenClaw delivers the one-time notice from
|
|
20872
|
+
// `before_agent_start` (a different process/turn than this scan) via
|
|
20873
|
+
// takePendingNotices, so the scan-time callback would never fire.
|
|
20874
|
+
modelDownloadWorkerPath: (0, import_node_path28.resolve)(__dirname, "model-download-worker.cjs"),
|
|
20875
|
+
skillUploadWorkerPath: (0, import_node_path28.resolve)(__dirname, "skill-upload-worker.cjs"),
|
|
20876
|
+
// Don't commit the "shown" flag here — commit happens at delivery.
|
|
20877
|
+
commitUploadNotice: false,
|
|
20878
|
+
announceCleanScans
|
|
19957
20879
|
});
|
|
19958
20880
|
}
|
|
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
|
-
|
|
20881
|
+
function createStartupScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
|
|
20882
|
+
return createOpenClawScanHandler(logger, "startup", branding2, onResult, announceCleanScans);
|
|
20883
|
+
}
|
|
20884
|
+
function createSessionScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
|
|
20885
|
+
return createOpenClawScanHandler(logger, "session", branding2, onResult, announceCleanScans);
|
|
20886
|
+
}
|
|
20887
|
+
function createBeforeAgentStartHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
|
|
20888
|
+
return (notices) => {
|
|
20889
|
+
const findings = getSecurityFindings();
|
|
20890
|
+
if (!notices && !findings) {
|
|
20891
|
+
logger.debug(`${branding2.name}: before_agent_start - nothing pending to surface`);
|
|
20892
|
+
return void 0;
|
|
20893
|
+
}
|
|
20894
|
+
if (findings) clearFindings();
|
|
20895
|
+
logger.debug(`${branding2.name}: surfacing context via before_agent_start`, {
|
|
20896
|
+
hasNotices: Boolean(notices),
|
|
20897
|
+
hasFindings: Boolean(findings)
|
|
20898
|
+
});
|
|
20899
|
+
const blocks = [];
|
|
20900
|
+
if (notices) {
|
|
20901
|
+
blocks.push(
|
|
20902
|
+
[
|
|
20903
|
+
'<sage-notice source="sage-session-start">',
|
|
20904
|
+
notices,
|
|
20905
|
+
"",
|
|
20906
|
+
"Relay this notice to the user. It is informational, not a security finding.",
|
|
20907
|
+
"</sage-notice>"
|
|
20908
|
+
].join("\n")
|
|
20909
|
+
);
|
|
20910
|
+
}
|
|
20911
|
+
if (findings) {
|
|
20912
|
+
blocks.push(
|
|
20913
|
+
[
|
|
20914
|
+
'<security-alert source="sage-plugin-scan">',
|
|
20915
|
+
findings,
|
|
20916
|
+
"",
|
|
20917
|
+
"Inform the user about these security findings.",
|
|
20918
|
+
"</security-alert>"
|
|
20919
|
+
].join("\n")
|
|
20920
|
+
);
|
|
20921
|
+
}
|
|
20922
|
+
return { prependContext: blocks.join("\n\n") };
|
|
19979
20923
|
};
|
|
19980
20924
|
}
|
|
19981
20925
|
|
|
@@ -20062,6 +21006,17 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
|
|
|
20062
21006
|
});
|
|
20063
21007
|
try {
|
|
20064
21008
|
const { toolName, params } = event;
|
|
21009
|
+
if (captureEnabled()) {
|
|
21010
|
+
await captureHookInput(
|
|
21011
|
+
"PreToolUse",
|
|
21012
|
+
{ toolName, params, sessionKey: ctx?.sessionKey },
|
|
21013
|
+
{
|
|
21014
|
+
toolName: canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName),
|
|
21015
|
+
toolInput: normalizeToolInput(toolName, params)
|
|
21016
|
+
},
|
|
21017
|
+
{ filePrefix: "openclaw-" }
|
|
21018
|
+
);
|
|
21019
|
+
}
|
|
20065
21020
|
const artifacts = mapToolToArtifacts(toolName, params);
|
|
20066
21021
|
if (!artifacts || artifacts.length === 0) {
|
|
20067
21022
|
completeHook("skipped", { skippedReason: "no_artifacts" });
|
|
@@ -20187,9 +21142,9 @@ ${notice}` : notice;
|
|
|
20187
21142
|
const onFindings = (msg) => {
|
|
20188
21143
|
pendingScanFindings = msg;
|
|
20189
21144
|
};
|
|
20190
|
-
const
|
|
21145
|
+
const getPendingSecurityFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
|
|
20191
21146
|
const beforeAgentStartHandler = createBeforeAgentStartHandler(
|
|
20192
|
-
|
|
21147
|
+
getPendingSecurityFindings,
|
|
20193
21148
|
() => {
|
|
20194
21149
|
pendingConfigurationWarnings = null;
|
|
20195
21150
|
pendingScanFindings = null;
|
|
@@ -20197,16 +21152,37 @@ ${notice}` : notice;
|
|
|
20197
21152
|
logger,
|
|
20198
21153
|
branding
|
|
20199
21154
|
);
|
|
21155
|
+
const resolveNoticeText = async () => {
|
|
21156
|
+
try {
|
|
21157
|
+
const ids = await takePendingNotices({ logger });
|
|
21158
|
+
const text = ids.map((id) => formatNoticeById(id, branding)).filter(Boolean).join("\n\n");
|
|
21159
|
+
logger.debug(`${branding.name}: notices resolved for delivery`, {
|
|
21160
|
+
noticeIds: ids,
|
|
21161
|
+
willShow: text.length > 0
|
|
21162
|
+
});
|
|
21163
|
+
return text.length > 0 ? text : null;
|
|
21164
|
+
} catch (e) {
|
|
21165
|
+
logger.warn(`${branding.name}: notice resolution failed`, { error: String(e) });
|
|
21166
|
+
return null;
|
|
21167
|
+
}
|
|
21168
|
+
};
|
|
20200
21169
|
api.on(
|
|
20201
21170
|
"before_tool_call",
|
|
20202
21171
|
createToolCallHandler(approvalStore, toolLogger, threatsDir, trustedDomainsDir, branding),
|
|
20203
21172
|
{ priority: 100 }
|
|
20204
21173
|
);
|
|
20205
|
-
api.on(
|
|
20206
|
-
|
|
21174
|
+
api.on(
|
|
21175
|
+
"gateway_start",
|
|
21176
|
+
createStartupScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
|
|
21177
|
+
);
|
|
21178
|
+
api.on(
|
|
21179
|
+
"session_start",
|
|
21180
|
+
createSessionScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
|
|
21181
|
+
);
|
|
20207
21182
|
api.on("before_agent_start", async () => {
|
|
20208
21183
|
await migrationCheckPromise;
|
|
20209
|
-
|
|
21184
|
+
const notices = await resolveNoticeText();
|
|
21185
|
+
return beforeAgentStartHandler(notices);
|
|
20210
21186
|
});
|
|
20211
21187
|
}
|
|
20212
21188
|
};
|