@gendigital/sage-openclaw 0.10.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 +5 -5
- package/dist/index.cjs +1881 -977
- package/dist/index.cjs.map +4 -4
- package/dist/mcp-server.cjs +31882 -0
- package/dist/mcp-server.cjs.map +7 -0
- package/dist/model-download-worker.cjs +140 -111
- 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 +9 -3
- package/resources/skills/security-awareness/SKILL.md +5 -1
- package/resources/threats/agent-layer.yaml +186 -0
- package/resources/threats/commands.yaml +24 -53
- package/resources/threats/credentials.yaml +12 -20
- package/resources/threats/dummy.yaml +44 -50
- package/resources/threats/files.yaml +34 -53
- package/resources/threats/mac-commands.yaml +20 -47
- package/resources/threats/mac-credentials.yaml +4 -12
- package/resources/threats/mac-files.yaml +5 -15
- package/resources/threats/mac-obfuscation.yaml +6 -13
- package/resources/threats/mac-persistence.yaml +11 -20
- package/resources/threats/mac-supply-chain.yaml +5 -8
- package/resources/threats/mitre.yaml +105 -167
- package/resources/threats/obfuscation.yaml +7 -15
- package/resources/threats/persistence.yaml +10 -17
- package/resources/threats/prompt-injection.yaml +92 -43
- package/resources/threats/self-defense.yaml +8 -12
- package/resources/threats/supply_chain.yaml +1 -2
- package/resources/threats/urls.yaml +9 -14
- package/resources/threats/win-commands.yaml +46 -106
- package/resources/threats/win-credentials.yaml +14 -24
- package/resources/threats/win-files.yaml +10 -20
- package/resources/threats/win-obfuscation.yaml +21 -37
- package/resources/threats/win-persistence.yaml +8 -16
- package/resources/threats/win-supply-chain.yaml +2 -3
- /package/resources/{allowlists → trusted-domains}/trusted-installer-domains.yaml +0 -0
package/dist/index.cjs
CHANGED
|
@@ -49,13 +49,31 @@ function getProcEnv() {
|
|
|
49
49
|
function getHomeDir() {
|
|
50
50
|
return getProcEnv().HOME || (0, import_node_os.homedir)();
|
|
51
51
|
}
|
|
52
|
+
async function renameWithRetry(from, to, attempts = 5) {
|
|
53
|
+
if (process.platform !== "win32") {
|
|
54
|
+
await fsPromises.rename(from, to);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (let i = 0; i < attempts; i++) {
|
|
58
|
+
try {
|
|
59
|
+
await fsPromises.rename(from, to);
|
|
60
|
+
return;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const code = err.code;
|
|
63
|
+
const transient = code === "EPERM" || code === "EACCES" || code === "EBUSY";
|
|
64
|
+
if (!transient || i === attempts - 1)
|
|
65
|
+
throw err;
|
|
66
|
+
await new Promise((resolve10) => setTimeout(resolve10, 10 * 2 ** i));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
52
70
|
async function atomicWriteJson(path, data) {
|
|
53
71
|
await fsPromises.mkdir((0, import_node_path.dirname)(path), { recursive: true });
|
|
54
72
|
const tmp = `${path}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`;
|
|
55
73
|
try {
|
|
56
74
|
await fsPromises.writeFile(tmp, `${JSON.stringify(data, null, 2)}
|
|
57
75
|
`, { mode: 384 });
|
|
58
|
-
await
|
|
76
|
+
await renameWithRetry(tmp, path);
|
|
59
77
|
} catch (err) {
|
|
60
78
|
try {
|
|
61
79
|
await fsPromises.unlink(tmp);
|
|
@@ -112,7 +130,47 @@ async function pruneOrphanedModelDownloads(stagingDir, maxAgeMs = 60 * 60 * 1e3)
|
|
|
112
130
|
}
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
|
-
|
|
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;
|
|
116
174
|
var init_file_utils = __esm({
|
|
117
175
|
"../core/dist/file-utils.js"() {
|
|
118
176
|
"use strict";
|
|
@@ -121,6 +179,7 @@ var init_file_utils = __esm({
|
|
|
121
179
|
fsPromises = __toESM(require("node:fs/promises"), 1);
|
|
122
180
|
import_node_os = require("node:os");
|
|
123
181
|
import_node_path = require("node:path");
|
|
182
|
+
import_promises = require("node:timers/promises");
|
|
124
183
|
name1 = "read";
|
|
125
184
|
name2 = "File";
|
|
126
185
|
}
|
|
@@ -4233,7 +4292,7 @@ var init_zod = __esm({
|
|
|
4233
4292
|
});
|
|
4234
4293
|
|
|
4235
4294
|
// ../core/dist/types.js
|
|
4236
|
-
var nullLogger, ArtifactTypeSchema, ArtifactSchema,
|
|
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;
|
|
4237
4296
|
var init_types2 = __esm({
|
|
4238
4297
|
"../core/dist/types.js"() {
|
|
4239
4298
|
"use strict";
|
|
@@ -4254,23 +4313,21 @@ var init_types2 = __esm({
|
|
|
4254
4313
|
value: external_exports.string(),
|
|
4255
4314
|
context: external_exports.string().optional()
|
|
4256
4315
|
});
|
|
4257
|
-
|
|
4258
|
-
ActionSchema = external_exports.enum(["block", "require_approval", "log"]);
|
|
4316
|
+
VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
|
|
4259
4317
|
ThreatSchema = external_exports.object({
|
|
4260
4318
|
id: external_exports.string(),
|
|
4261
4319
|
version: external_exports.number().int().optional(),
|
|
4262
4320
|
category: external_exports.string(),
|
|
4263
|
-
severity:
|
|
4321
|
+
severity: VerdictSeveritySchema,
|
|
4264
4322
|
confidence: external_exports.number(),
|
|
4265
|
-
action: ActionSchema,
|
|
4266
4323
|
pattern: external_exports.string(),
|
|
4267
4324
|
match_on: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]),
|
|
4268
4325
|
title: external_exports.string(),
|
|
4269
4326
|
expires_at: external_exports.string().nullable().optional(),
|
|
4270
|
-
revoked: external_exports.boolean().optional().default(false)
|
|
4327
|
+
revoked: external_exports.boolean().optional().default(false),
|
|
4328
|
+
flags: external_exports.array(external_exports.string()).optional().default([])
|
|
4271
4329
|
});
|
|
4272
4330
|
DecisionSchema = external_exports.enum(["allow", "deny", "ask"]);
|
|
4273
|
-
VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
|
|
4274
4331
|
SensitivitySchema = external_exports.enum(["paranoid", "balanced", "relaxed"]);
|
|
4275
4332
|
UrlCheckConfigSchema = external_exports.object({
|
|
4276
4333
|
endpoint: external_exports.string().optional(),
|
|
@@ -4283,9 +4340,6 @@ var init_types2 = __esm({
|
|
|
4283
4340
|
ttl_clean_seconds: external_exports.number().default(86400),
|
|
4284
4341
|
path: external_exports.string().default("~/.sage/cache.json")
|
|
4285
4342
|
});
|
|
4286
|
-
AllowlistConfigSchema = external_exports.object({
|
|
4287
|
-
path: external_exports.string().default("~/.sage/allowlist.json")
|
|
4288
|
-
});
|
|
4289
4343
|
LoggingConfigSchema = external_exports.object({
|
|
4290
4344
|
enabled: external_exports.boolean().default(true),
|
|
4291
4345
|
log_clean: external_exports.boolean().default(false),
|
|
@@ -4315,14 +4369,24 @@ var init_types2 = __esm({
|
|
|
4315
4369
|
AmsiCheckConfigSchema = external_exports.object({
|
|
4316
4370
|
enabled: external_exports.boolean().default(true)
|
|
4317
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
|
+
});
|
|
4318
4383
|
DEFAULT_PI_HIGH_RISK_THRESHOLD = 0.99;
|
|
4319
4384
|
DEFAULT_PI_MEDIUM_RISK_THRESHOLD = 0.5;
|
|
4385
|
+
DEFAULT_PI_TELEMETRY_THRESHOLD = 0.95;
|
|
4320
4386
|
PiCheckConfigSchema = external_exports.object({
|
|
4321
4387
|
enabled: external_exports.boolean().default(false),
|
|
4322
4388
|
max_content_length: external_exports.number().default(16384),
|
|
4323
|
-
model_path: external_exports.string().optional()
|
|
4324
|
-
high_risk_threshold: external_exports.number().default(DEFAULT_PI_HIGH_RISK_THRESHOLD),
|
|
4325
|
-
medium_risk_threshold: external_exports.number().default(DEFAULT_PI_MEDIUM_RISK_THRESHOLD)
|
|
4389
|
+
model_path: external_exports.string().optional()
|
|
4326
4390
|
});
|
|
4327
4391
|
ExceptionDecisionSchema = external_exports.enum(["allow", "deny"]);
|
|
4328
4392
|
ExceptionMatchSchema = external_exports.enum(["executable", "domain", "path", "plugin", "regex"]);
|
|
@@ -4344,15 +4408,16 @@ var init_types2 = __esm({
|
|
|
4344
4408
|
file_check: FileCheckConfigSchema.default({}),
|
|
4345
4409
|
package_check: PackageCheckConfigSchema.default({}),
|
|
4346
4410
|
amsi_check: AmsiCheckConfigSchema.default({}),
|
|
4411
|
+
skill_check: SkillCheckConfigSchema.default({}),
|
|
4347
4412
|
pi_check: PiCheckConfigSchema.default({}),
|
|
4348
4413
|
heuristics_enabled: external_exports.boolean().default(true),
|
|
4349
4414
|
cache: CacheConfigSchema.default({}),
|
|
4350
|
-
allowlist: AllowlistConfigSchema.default({}),
|
|
4351
4415
|
exceptions: ExceptionsConfigSchema.default({}),
|
|
4352
4416
|
logging: LoggingConfigSchema.default({}),
|
|
4353
4417
|
operational_logging: OperationalLoggingConfigSchema.default({}),
|
|
4354
4418
|
sensitivity: SensitivitySchema.default("balanced"),
|
|
4355
4419
|
disabled_threats: external_exports.array(external_exports.string()).default([]),
|
|
4420
|
+
announce_clean_scans: external_exports.boolean().default(true),
|
|
4356
4421
|
brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
|
|
4357
4422
|
community_iq: external_exports.boolean().default(true)
|
|
4358
4423
|
});
|
|
@@ -4371,12 +4436,27 @@ var init_types2 = __esm({
|
|
|
4371
4436
|
var config_exports = {};
|
|
4372
4437
|
__export(config_exports, {
|
|
4373
4438
|
HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
|
|
4439
|
+
MS_PER_DAY: () => MS_PER_DAY,
|
|
4374
4440
|
SAGE_DIR: () => SAGE_DIR,
|
|
4375
4441
|
defaultConfigPath: () => defaultConfigPath,
|
|
4442
|
+
getClaudeConfigDir: () => getClaudeConfigDir,
|
|
4443
|
+
isFreshTimestamp: () => isFreshTimestamp,
|
|
4376
4444
|
loadConfig: () => loadConfig,
|
|
4377
4445
|
loadConfigSync: () => loadConfigSync,
|
|
4378
|
-
|
|
4446
|
+
readExplicitSkillUploadEnabled: () => readExplicitSkillUploadEnabled,
|
|
4447
|
+
resolvePath: () => resolvePath,
|
|
4448
|
+
skillCacheTtlMs: () => skillCacheTtlMs
|
|
4379
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
|
+
}
|
|
4380
4460
|
function resolvedSageDir() {
|
|
4381
4461
|
return resolvePath(SAGE_DIR);
|
|
4382
4462
|
}
|
|
@@ -4386,9 +4466,6 @@ function defaultConfigPath() {
|
|
|
4386
4466
|
function defaultCachePath() {
|
|
4387
4467
|
return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
|
|
4388
4468
|
}
|
|
4389
|
-
function defaultAllowlistPath() {
|
|
4390
|
-
return (0, import_node_path2.join)(resolvedSageDir(), "allowlist.json");
|
|
4391
|
-
}
|
|
4392
4469
|
function defaultExceptionsPath() {
|
|
4393
4470
|
return (0, import_node_path2.join)(resolvedSageDir(), "exceptions.json");
|
|
4394
4471
|
}
|
|
@@ -4405,6 +4482,12 @@ function resolvePath(pathStr) {
|
|
|
4405
4482
|
}
|
|
4406
4483
|
return pathStr;
|
|
4407
4484
|
}
|
|
4485
|
+
function getClaudeConfigDir() {
|
|
4486
|
+
const envDir = process.env.CLAUDE_CONFIG_DIR;
|
|
4487
|
+
if (envDir)
|
|
4488
|
+
return resolvePath(envDir);
|
|
4489
|
+
return resolvePath("~/.claude");
|
|
4490
|
+
}
|
|
4408
4491
|
function isWithinDirectory(baseDir, targetPath) {
|
|
4409
4492
|
const rel = (0, import_node_path2.relative)(baseDir, targetPath);
|
|
4410
4493
|
if (rel === "")
|
|
@@ -4448,13 +4531,12 @@ function sanitizeBrandKey(data, logger) {
|
|
|
4448
4531
|
if (typeof brandKey === "string" && brandKey.length >= 1 && brandKey.length <= 32 && BRAND_KEY_RE.test(brandKey)) {
|
|
4449
4532
|
return data;
|
|
4450
4533
|
}
|
|
4451
|
-
logger.warn(`Invalid brand_key in config
|
|
4534
|
+
logger.warn(`Invalid brand_key in config - ignoring`, { brand_key: brandKey });
|
|
4452
4535
|
const { brand_key: _, ...rest } = data;
|
|
4453
4536
|
return rest;
|
|
4454
4537
|
}
|
|
4455
4538
|
function sanitizeConfigPaths(config2, logger) {
|
|
4456
4539
|
const cachePath = defaultCachePath();
|
|
4457
|
-
const allowlistPath = defaultAllowlistPath();
|
|
4458
4540
|
const exceptionsPath = defaultExceptionsPath();
|
|
4459
4541
|
const auditPath = defaultAuditPath();
|
|
4460
4542
|
const operationalLogPath = defaultOperationalLogPath();
|
|
@@ -4464,10 +4546,6 @@ function sanitizeConfigPaths(config2, logger) {
|
|
|
4464
4546
|
...config2.cache,
|
|
4465
4547
|
path: normalizeStateFilePath(config2.cache.path, cachePath, "cache", logger)
|
|
4466
4548
|
},
|
|
4467
|
-
allowlist: {
|
|
4468
|
-
...config2.allowlist,
|
|
4469
|
-
path: normalizeStateFilePath(config2.allowlist.path, allowlistPath, "allowlist", logger)
|
|
4470
|
-
},
|
|
4471
4549
|
exceptions: {
|
|
4472
4550
|
...config2.exceptions,
|
|
4473
4551
|
path: normalizeStateFilePath(config2.exceptions.path, exceptionsPath, "exceptions", logger)
|
|
@@ -4505,6 +4583,21 @@ function parseConfig(raw, path, logger) {
|
|
|
4505
4583
|
return defaultConfig(logger);
|
|
4506
4584
|
}
|
|
4507
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
|
+
}
|
|
4508
4601
|
async function loadConfig(configPath, logger = nullLogger) {
|
|
4509
4602
|
const path = configPath ? resolvePath(configPath) : defaultConfigPath();
|
|
4510
4603
|
try {
|
|
@@ -4521,7 +4614,7 @@ function loadConfigSync(configPath, logger = nullLogger) {
|
|
|
4521
4614
|
return defaultConfig(logger);
|
|
4522
4615
|
}
|
|
4523
4616
|
}
|
|
4524
|
-
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;
|
|
4525
4618
|
var init_config = __esm({
|
|
4526
4619
|
"../core/dist/config.js"() {
|
|
4527
4620
|
"use strict";
|
|
@@ -4530,6 +4623,8 @@ var init_config = __esm({
|
|
|
4530
4623
|
init_types2();
|
|
4531
4624
|
SAGE_DIR = "~/.sage";
|
|
4532
4625
|
HOOK_TIMEOUT_SECONDS = 8;
|
|
4626
|
+
MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
4627
|
+
CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1e3;
|
|
4533
4628
|
BRAND_KEY_RE = /^[a-z0-9_-]+$/u;
|
|
4534
4629
|
}
|
|
4535
4630
|
});
|
|
@@ -6794,7 +6889,7 @@ async function ensurePiDeps(modelPath, logger = nullLogger) {
|
|
|
6794
6889
|
}
|
|
6795
6890
|
}
|
|
6796
6891
|
function runNpm(cwd, args, logger) {
|
|
6797
|
-
return new Promise((
|
|
6892
|
+
return new Promise((resolve10, reject) => {
|
|
6798
6893
|
const isWindows = process.platform === "win32";
|
|
6799
6894
|
const npmCmd = isWindows ? "npm.cmd" : "npm";
|
|
6800
6895
|
const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
|
|
@@ -6802,7 +6897,7 @@ function runNpm(cwd, args, logger) {
|
|
|
6802
6897
|
logger.warn(`npm install error: ${stderr || error.message}`);
|
|
6803
6898
|
reject(error);
|
|
6804
6899
|
} else {
|
|
6805
|
-
|
|
6900
|
+
resolve10();
|
|
6806
6901
|
}
|
|
6807
6902
|
});
|
|
6808
6903
|
child.unref();
|
|
@@ -14115,124 +14210,66 @@ __export(index_exports, {
|
|
|
14115
14210
|
});
|
|
14116
14211
|
module.exports = __toCommonJS(index_exports);
|
|
14117
14212
|
|
|
14118
|
-
// ../core/dist/allowlist.js
|
|
14119
|
-
init_config();
|
|
14120
|
-
init_file_utils();
|
|
14121
|
-
init_types2();
|
|
14122
|
-
|
|
14123
|
-
// ../core/dist/url-utils.js
|
|
14124
|
-
var import_node_crypto2 = require("node:crypto");
|
|
14213
|
+
// ../core/dist/allowlist-migration.js
|
|
14125
14214
|
var import_node_path3 = require("node:path");
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
return
|
|
14215
|
+
|
|
14216
|
+
// ../core/dist/brands.js
|
|
14217
|
+
var defaultBranding = { name: "Sage", short_name: "Sage" };
|
|
14218
|
+
var BRANDS = {
|
|
14219
|
+
norton: { name: "Norton AI Agent Protection", short_name: "Norton" }
|
|
14220
|
+
};
|
|
14221
|
+
function resolveBranding(brandKey, logger) {
|
|
14222
|
+
if (!brandKey)
|
|
14223
|
+
return defaultBranding;
|
|
14224
|
+
const entry = BRANDS[brandKey];
|
|
14225
|
+
if (!entry) {
|
|
14226
|
+
logger?.warn(`Unknown brand_key "${brandKey}" in config - using default branding`);
|
|
14227
|
+
return defaultBranding;
|
|
14135
14228
|
}
|
|
14136
|
-
}
|
|
14137
|
-
function hashCommand(command) {
|
|
14138
|
-
return (0, import_node_crypto2.createHash)("sha256").update(command).digest("hex");
|
|
14139
|
-
}
|
|
14140
|
-
function normalizeFilePath(raw) {
|
|
14141
|
-
const home = getHomeDir();
|
|
14142
|
-
const expanded = raw.startsWith("~/") || raw === "~" ? `${home}${raw.slice(1)}` : raw;
|
|
14143
|
-
return (0, import_node_path3.normalize)(expanded);
|
|
14229
|
+
return { ...entry, brand_key: brandKey };
|
|
14144
14230
|
}
|
|
14145
14231
|
|
|
14146
|
-
// ../core/dist/allowlist.js
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
const entries = {};
|
|
14152
|
-
for (const [key, entryData] of Object.entries(raw)) {
|
|
14153
|
-
if (typeof entryData !== "object" || entryData === null)
|
|
14154
|
-
continue;
|
|
14155
|
-
const data = entryData;
|
|
14156
|
-
if (typeof data.added_at === "string" && typeof data.reason === "string" && typeof data.original_verdict === "string") {
|
|
14157
|
-
entries[key] = {
|
|
14158
|
-
addedAt: data.added_at,
|
|
14159
|
-
reason: data.reason,
|
|
14160
|
-
originalVerdict: data.original_verdict
|
|
14161
|
-
};
|
|
14162
|
-
}
|
|
14163
|
-
}
|
|
14164
|
-
return entries;
|
|
14232
|
+
// ../core/dist/allowlist-migration.js
|
|
14233
|
+
init_config();
|
|
14234
|
+
init_file_utils();
|
|
14235
|
+
function defaultAllowlistPath() {
|
|
14236
|
+
return (0, import_node_path3.join)(resolvePath("~/.sage"), "allowlist.json");
|
|
14165
14237
|
}
|
|
14166
|
-
async function
|
|
14167
|
-
const path = resolvePath(config2.path);
|
|
14168
|
-
let raw;
|
|
14238
|
+
async function checkAllowlistMigration() {
|
|
14169
14239
|
try {
|
|
14170
|
-
raw = await getFileContent(
|
|
14240
|
+
const raw = await getFileContent(defaultAllowlistPath());
|
|
14241
|
+
const data = JSON.parse(raw);
|
|
14242
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
14243
|
+
return { needed: false, entryTypes: [] };
|
|
14244
|
+
}
|
|
14245
|
+
const record = data;
|
|
14246
|
+
const found = ["urls", "commands", "file_paths"].filter((key) => typeof record[key] === "object" && record[key] !== null && Object.keys(record[key]).length > 0);
|
|
14247
|
+
return { needed: found.length > 0, entryTypes: found };
|
|
14171
14248
|
} catch {
|
|
14172
|
-
return
|
|
14173
|
-
}
|
|
14174
|
-
let data;
|
|
14175
|
-
try {
|
|
14176
|
-
data = JSON.parse(raw);
|
|
14177
|
-
} catch (e) {
|
|
14178
|
-
logger.warn(`Failed to load allowlist from ${path}`, { error: String(e) });
|
|
14179
|
-
return emptyAllowlist();
|
|
14249
|
+
return { needed: false, entryTypes: [] };
|
|
14180
14250
|
}
|
|
14181
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
14182
|
-
logger.warn(`Allowlist file ${path} does not contain a JSON object`);
|
|
14183
|
-
return emptyAllowlist();
|
|
14184
|
-
}
|
|
14185
|
-
const record = data;
|
|
14186
|
-
const rawUrls = parseEntries(record.urls ?? {});
|
|
14187
|
-
const urls = {};
|
|
14188
|
-
for (const [key, entry] of Object.entries(rawUrls)) {
|
|
14189
|
-
urls[normalizeUrl(key)] = entry;
|
|
14190
|
-
}
|
|
14191
|
-
const rawFilePaths = parseEntries(record.file_paths ?? {});
|
|
14192
|
-
const filePaths = {};
|
|
14193
|
-
for (const [key, entry] of Object.entries(rawFilePaths)) {
|
|
14194
|
-
filePaths[normalizeFilePath(key)] = entry;
|
|
14195
|
-
}
|
|
14196
|
-
return {
|
|
14197
|
-
urls,
|
|
14198
|
-
commands: parseEntries(record.commands ?? {}),
|
|
14199
|
-
filePaths
|
|
14200
|
-
};
|
|
14201
14251
|
}
|
|
14202
|
-
function
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
}
|
|
14212
|
-
for (const artifact of artifacts) {
|
|
14213
|
-
if (artifact.type !== "file_path") {
|
|
14214
|
-
continue;
|
|
14215
|
-
}
|
|
14216
|
-
if (normalizeFilePath(artifact.value) in allowlist.filePaths) {
|
|
14217
|
-
return true;
|
|
14218
|
-
}
|
|
14219
|
-
}
|
|
14220
|
-
if (artifacts.length > 0 && artifacts.every((artifact) => artifact.type === "url")) {
|
|
14221
|
-
return artifacts.every((artifact) => normalizeUrl(artifact.value) in allowlist.urls);
|
|
14222
|
-
}
|
|
14223
|
-
return false;
|
|
14252
|
+
function formatAllowlistMigrationWarning(entryTypes, branding2 = defaultBranding) {
|
|
14253
|
+
const typeList = entryTypes.join(", ");
|
|
14254
|
+
const lines = [
|
|
14255
|
+
`${branding2.name}: legacy allowlist.json detected with ${typeList} entries.`,
|
|
14256
|
+
" - URL and file_path entries can be migrated manually to ~/.sage/exceptions.json",
|
|
14257
|
+
" - Command entries are stored as SHA-256 hashes and cannot be recovered",
|
|
14258
|
+
" - See https://github.com/gendigitalinc/sage for migration instructions"
|
|
14259
|
+
];
|
|
14260
|
+
return lines.join("\n");
|
|
14224
14261
|
}
|
|
14225
14262
|
|
|
14226
14263
|
// ../core/dist/approval-store.js
|
|
14227
|
-
var
|
|
14264
|
+
var import_node_crypto2 = require("node:crypto");
|
|
14228
14265
|
var PENDING_STALE_MS = 60 * 60 * 1e3;
|
|
14229
14266
|
var APPROVED_TTL_MS = 10 * 60 * 1e3;
|
|
14230
|
-
var ApprovalStore = class
|
|
14267
|
+
var ApprovalStore = class {
|
|
14231
14268
|
pending = /* @__PURE__ */ new Map();
|
|
14232
14269
|
approved = /* @__PURE__ */ new Map();
|
|
14233
14270
|
static actionId(toolName, params, sessionId) {
|
|
14234
14271
|
const payload = JSON.stringify({ toolName, params, sessionId });
|
|
14235
|
-
return (0,
|
|
14272
|
+
return (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex").slice(0, 32);
|
|
14236
14273
|
}
|
|
14237
14274
|
static artifactId(type, value) {
|
|
14238
14275
|
return `${type}:${value}`;
|
|
@@ -14269,40 +14306,6 @@ var ApprovalStore = class _ApprovalStore {
|
|
|
14269
14306
|
}
|
|
14270
14307
|
return true;
|
|
14271
14308
|
}
|
|
14272
|
-
hasApprovedArtifact(type, value) {
|
|
14273
|
-
return this.findApprovedAction(type, value) !== null;
|
|
14274
|
-
}
|
|
14275
|
-
consumeApprovedArtifact(type, value) {
|
|
14276
|
-
const id = _ApprovalStore.artifactId(type, value);
|
|
14277
|
-
for (const [actionId, entry] of this.approved.entries()) {
|
|
14278
|
-
if (Date.now() >= entry.expiresAt) {
|
|
14279
|
-
this.approved.delete(actionId);
|
|
14280
|
-
continue;
|
|
14281
|
-
}
|
|
14282
|
-
const idx = entry.artifacts.findIndex((a) => _ApprovalStore.artifactId(a.type, a.value) === id);
|
|
14283
|
-
if (idx !== -1) {
|
|
14284
|
-
entry.artifacts.splice(idx, 1);
|
|
14285
|
-
if (entry.artifacts.length === 0) {
|
|
14286
|
-
this.approved.delete(actionId);
|
|
14287
|
-
}
|
|
14288
|
-
return true;
|
|
14289
|
-
}
|
|
14290
|
-
}
|
|
14291
|
-
return false;
|
|
14292
|
-
}
|
|
14293
|
-
findApprovedAction(type, value) {
|
|
14294
|
-
const id = _ApprovalStore.artifactId(type, value);
|
|
14295
|
-
for (const [actionId, entry] of this.approved.entries()) {
|
|
14296
|
-
if (Date.now() >= entry.expiresAt) {
|
|
14297
|
-
this.approved.delete(actionId);
|
|
14298
|
-
continue;
|
|
14299
|
-
}
|
|
14300
|
-
if (entry.artifacts.some((artifact) => _ApprovalStore.artifactId(artifact.type, artifact.value) === id)) {
|
|
14301
|
-
return actionId;
|
|
14302
|
-
}
|
|
14303
|
-
}
|
|
14304
|
-
return null;
|
|
14305
|
-
}
|
|
14306
14309
|
cleanup(now = Date.now()) {
|
|
14307
14310
|
for (const [actionId, entry] of this.pending.entries()) {
|
|
14308
14311
|
if (now - entry.createdAt >= PENDING_STALE_MS) {
|
|
@@ -14318,99 +14321,237 @@ var ApprovalStore = class _ApprovalStore {
|
|
|
14318
14321
|
};
|
|
14319
14322
|
|
|
14320
14323
|
// ../core/dist/audit-log.js
|
|
14321
|
-
var
|
|
14324
|
+
var import_node_crypto3 = require("node:crypto");
|
|
14322
14325
|
init_config();
|
|
14323
|
-
init_file_utils();
|
|
14324
14326
|
|
|
14325
|
-
// ../core/dist/
|
|
14326
|
-
var
|
|
14327
|
-
var
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
return
|
|
14340
|
-
|
|
14341
|
-
|
|
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);
|
|
14342
14346
|
}
|
|
14347
|
+
return value.slice(0, cutIndex);
|
|
14343
14348
|
}
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
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)}`;
|
|
14351
14361
|
}
|
|
14362
|
+
return value;
|
|
14352
14363
|
}
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
const deadline = Date.now() + ROTATE_LOCK_TIMEOUT_MS;
|
|
14356
|
-
while (true) {
|
|
14357
|
-
try {
|
|
14358
|
-
await (0, import_promises.mkdir)(lockPath);
|
|
14359
|
-
return async () => {
|
|
14360
|
-
try {
|
|
14361
|
-
await (0, import_promises.rmdir)(lockPath);
|
|
14362
|
-
} catch {
|
|
14363
|
-
}
|
|
14364
|
-
};
|
|
14365
|
-
} catch {
|
|
14366
|
-
await removeStaleRotateLock(lockPath);
|
|
14367
|
-
const remainingMs = deadline - Date.now();
|
|
14368
|
-
if (remainingMs <= 0)
|
|
14369
|
-
return void 0;
|
|
14370
|
-
await (0, import_promises2.setTimeout)(Math.min(ROTATE_LOCK_POLL_MS, remainingMs));
|
|
14371
|
-
}
|
|
14372
|
-
}
|
|
14364
|
+
function asString(v) {
|
|
14365
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
14373
14366
|
}
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
}
|
|
14387
|
-
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
14388
|
-
try {
|
|
14389
|
-
await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
|
|
14390
|
-
} catch {
|
|
14391
|
-
}
|
|
14392
|
-
}
|
|
14393
|
-
try {
|
|
14394
|
-
await (0, import_promises.rename)(filePath, `${filePath}.1`);
|
|
14395
|
-
} catch {
|
|
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;
|
|
14396
14379
|
}
|
|
14397
|
-
} finally {
|
|
14398
|
-
await releaseLock();
|
|
14399
14380
|
}
|
|
14381
|
+
return void 0;
|
|
14400
14382
|
}
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
const
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
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
|
|
14502
|
+
init_file_utils();
|
|
14503
|
+
|
|
14504
|
+
// ../core/dist/jsonl-log-writer.js
|
|
14505
|
+
var import_promises2 = require("node:fs/promises");
|
|
14506
|
+
var import_node_path4 = require("node:path");
|
|
14507
|
+
init_config();
|
|
14508
|
+
init_file_utils();
|
|
14509
|
+
var writeQueues = /* @__PURE__ */ new Map();
|
|
14510
|
+
async function shouldRotate(filePath, maxBytes, maxFiles) {
|
|
14511
|
+
if (maxBytes <= 0 || maxFiles <= 0)
|
|
14512
|
+
return false;
|
|
14513
|
+
try {
|
|
14514
|
+
const s = await (0, import_promises2.stat)(filePath);
|
|
14515
|
+
return s.size >= maxBytes;
|
|
14516
|
+
} catch {
|
|
14517
|
+
return false;
|
|
14518
|
+
}
|
|
14519
|
+
}
|
|
14520
|
+
async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
|
|
14521
|
+
if (!await shouldRotate(filePath, maxBytes, maxFiles))
|
|
14522
|
+
return;
|
|
14523
|
+
await withFileLock(filePath, async () => {
|
|
14524
|
+
if (!await shouldRotate(filePath, maxBytes, maxFiles))
|
|
14525
|
+
return;
|
|
14526
|
+
try {
|
|
14527
|
+
await (0, import_promises2.unlink)(`${filePath}.${maxFiles}`);
|
|
14528
|
+
} catch {
|
|
14529
|
+
}
|
|
14530
|
+
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
14531
|
+
try {
|
|
14532
|
+
await (0, import_promises2.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
|
|
14533
|
+
} catch {
|
|
14534
|
+
}
|
|
14535
|
+
}
|
|
14536
|
+
try {
|
|
14537
|
+
await (0, import_promises2.rename)(filePath, `${filePath}.1`);
|
|
14538
|
+
} catch {
|
|
14539
|
+
}
|
|
14540
|
+
});
|
|
14541
|
+
}
|
|
14542
|
+
async function appendJsonlEntryNow(path, config2, entry) {
|
|
14543
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
14544
|
+
await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
|
|
14545
|
+
await (0, import_promises2.appendFile)(path, `${JSON.stringify(entry)}
|
|
14546
|
+
`);
|
|
14547
|
+
}
|
|
14548
|
+
async function appendJsonlEntry(config2, entry) {
|
|
14549
|
+
const path = resolvePath(config2.path);
|
|
14550
|
+
const previousWrite = writeQueues.get(path) ?? Promise.resolve();
|
|
14551
|
+
const nextWrite = previousWrite.catch(() => {
|
|
14552
|
+
}).then(() => appendJsonlEntryNow(path, config2, entry));
|
|
14553
|
+
writeQueues.set(path, nextWrite);
|
|
14554
|
+
try {
|
|
14414
14555
|
await nextWrite;
|
|
14415
14556
|
} finally {
|
|
14416
14557
|
if (writeQueues.get(path) === nextWrite)
|
|
@@ -14419,6 +14560,7 @@ async function appendJsonlEntry(config2, entry) {
|
|
|
14419
14560
|
}
|
|
14420
14561
|
|
|
14421
14562
|
// ../core/dist/audit-log.js
|
|
14563
|
+
init_types2();
|
|
14422
14564
|
var MAX_SUMMARY_LEN = 200;
|
|
14423
14565
|
var AUDIT_LOG_SCHEMA_VERSION = 1;
|
|
14424
14566
|
function toolInputSummary(toolName, toolInput) {
|
|
@@ -14446,7 +14588,7 @@ async function logVerdict(config2, input) {
|
|
|
14446
14588
|
return;
|
|
14447
14589
|
const entry = {
|
|
14448
14590
|
type: "runtime_verdict",
|
|
14449
|
-
entry_id: input.eventId ?? (0,
|
|
14591
|
+
entry_id: input.eventId ?? (0, import_node_crypto3.randomUUID)(),
|
|
14450
14592
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14451
14593
|
session_id: input.sessionId,
|
|
14452
14594
|
conversation_id: input.conversationId ?? input.sessionId,
|
|
@@ -14487,32 +14629,46 @@ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
|
|
|
14487
14629
|
} catch {
|
|
14488
14630
|
}
|
|
14489
14631
|
}
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
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 {
|
|
14503
14646
|
}
|
|
14504
|
-
return { ...entry, brand_key: brandKey };
|
|
14505
14647
|
}
|
|
14506
14648
|
|
|
14507
14649
|
// ../core/dist/cache.js
|
|
14508
14650
|
init_config();
|
|
14509
14651
|
init_file_utils();
|
|
14510
14652
|
init_types2();
|
|
14653
|
+
|
|
14654
|
+
// ../core/dist/url-utils.js
|
|
14655
|
+
init_file_utils();
|
|
14656
|
+
function normalizeUrl(raw) {
|
|
14657
|
+
try {
|
|
14658
|
+
const u = new URL(raw);
|
|
14659
|
+
u.hash = "";
|
|
14660
|
+
u.searchParams.sort();
|
|
14661
|
+
return u.toString();
|
|
14662
|
+
} catch {
|
|
14663
|
+
return raw.toLowerCase();
|
|
14664
|
+
}
|
|
14665
|
+
}
|
|
14666
|
+
|
|
14667
|
+
// ../core/dist/cache.js
|
|
14511
14668
|
var ONE_HOUR = 3600;
|
|
14512
14669
|
var TWENTY_FOUR_HOURS = 86400;
|
|
14513
|
-
var FAR_FUTURE = "9999-12-31T23:59:59+00:00";
|
|
14514
14670
|
var VerdictCache = class {
|
|
14515
|
-
store = { urls: {},
|
|
14671
|
+
store = { urls: {}, packages: {} };
|
|
14516
14672
|
path;
|
|
14517
14673
|
config;
|
|
14518
14674
|
logger;
|
|
@@ -14531,11 +14687,10 @@ var VerdictCache = class {
|
|
|
14531
14687
|
const data = JSON.parse(raw);
|
|
14532
14688
|
this.store = {
|
|
14533
14689
|
urls: data.urls ?? {},
|
|
14534
|
-
commands: data.commands ?? {},
|
|
14535
14690
|
packages: data.packages ?? {}
|
|
14536
14691
|
};
|
|
14537
14692
|
} catch {
|
|
14538
|
-
this.store = { urls: {},
|
|
14693
|
+
this.store = { urls: {}, packages: {} };
|
|
14539
14694
|
}
|
|
14540
14695
|
}
|
|
14541
14696
|
getUrl(url) {
|
|
@@ -14567,31 +14722,6 @@ var VerdictCache = class {
|
|
|
14567
14722
|
sageVersion: this.version
|
|
14568
14723
|
};
|
|
14569
14724
|
}
|
|
14570
|
-
getCommand(commandHash) {
|
|
14571
|
-
if (!this.config.enabled)
|
|
14572
|
-
return null;
|
|
14573
|
-
const entry = this.store.commands[commandHash];
|
|
14574
|
-
if (!entry || this.isStale(entry))
|
|
14575
|
-
return null;
|
|
14576
|
-
return {
|
|
14577
|
-
verdict: entry.verdict,
|
|
14578
|
-
severity: entry.severity,
|
|
14579
|
-
reasons: entry.reasons,
|
|
14580
|
-
source: entry.source
|
|
14581
|
-
};
|
|
14582
|
-
}
|
|
14583
|
-
putCommand(commandHash, verdict) {
|
|
14584
|
-
if (!this.config.enabled)
|
|
14585
|
-
return;
|
|
14586
|
-
const { urlSignalLabels: _ignoredUrlLabels, ...rest } = verdict;
|
|
14587
|
-
const now = /* @__PURE__ */ new Date();
|
|
14588
|
-
this.store.commands[commandHash] = {
|
|
14589
|
-
...rest,
|
|
14590
|
-
checkedAt: now.toISOString(),
|
|
14591
|
-
expiresAt: FAR_FUTURE,
|
|
14592
|
-
sageVersion: this.version
|
|
14593
|
-
};
|
|
14594
|
-
}
|
|
14595
14725
|
getPackage(key) {
|
|
14596
14726
|
if (!this.config.enabled)
|
|
14597
14727
|
return null;
|
|
@@ -14602,7 +14732,9 @@ var VerdictCache = class {
|
|
|
14602
14732
|
verdict: entry.verdict,
|
|
14603
14733
|
severity: entry.severity,
|
|
14604
14734
|
reasons: entry.reasons,
|
|
14605
|
-
source: entry.source
|
|
14735
|
+
source: entry.source,
|
|
14736
|
+
...entry.packageVerdict !== void 0 ? { packageVerdict: entry.packageVerdict } : {},
|
|
14737
|
+
...entry.packageConfidence !== void 0 ? { packageConfidence: entry.packageConfidence } : {}
|
|
14606
14738
|
};
|
|
14607
14739
|
}
|
|
14608
14740
|
putPackage(key, verdict, packageAgeDays) {
|
|
@@ -14652,10 +14784,6 @@ var VerdictCache = class {
|
|
|
14652
14784
|
if (this.isStale(entry))
|
|
14653
14785
|
delete this.store.urls[key];
|
|
14654
14786
|
}
|
|
14655
|
-
for (const [key, entry] of Object.entries(this.store.commands)) {
|
|
14656
|
-
if (this.isStale(entry))
|
|
14657
|
-
delete this.store.commands[key];
|
|
14658
|
-
}
|
|
14659
14787
|
for (const [key, entry] of Object.entries(this.store.packages)) {
|
|
14660
14788
|
if (this.isStale(entry))
|
|
14661
14789
|
delete this.store.packages[key];
|
|
@@ -14947,6 +15075,7 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
14947
15075
|
invalidResultLogged = false;
|
|
14948
15076
|
stdoutBuffer = "";
|
|
14949
15077
|
pendingResponse = null;
|
|
15078
|
+
scanQueue = Promise.resolve();
|
|
14950
15079
|
constructor(logger) {
|
|
14951
15080
|
this.logger = logger;
|
|
14952
15081
|
}
|
|
@@ -14954,13 +15083,26 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
14954
15083
|
return this.available;
|
|
14955
15084
|
}
|
|
14956
15085
|
waitForLine(timeout) {
|
|
14957
|
-
return new Promise((
|
|
15086
|
+
return new Promise((resolve10, reject) => {
|
|
14958
15087
|
const timer = setTimeout(() => {
|
|
14959
15088
|
this.pendingResponse = null;
|
|
14960
15089
|
reject(new Error("timeout"));
|
|
14961
15090
|
}, timeout);
|
|
14962
|
-
this.pendingResponse = { resolve:
|
|
15091
|
+
this.pendingResponse = { resolve: resolve10, reject, timer };
|
|
15092
|
+
});
|
|
15093
|
+
}
|
|
15094
|
+
async enqueueScan(operation) {
|
|
15095
|
+
const previous = this.scanQueue;
|
|
15096
|
+
let release;
|
|
15097
|
+
this.scanQueue = new Promise((resolve10) => {
|
|
15098
|
+
release = resolve10;
|
|
14963
15099
|
});
|
|
15100
|
+
await previous;
|
|
15101
|
+
try {
|
|
15102
|
+
return await operation();
|
|
15103
|
+
} finally {
|
|
15104
|
+
release();
|
|
15105
|
+
}
|
|
14964
15106
|
}
|
|
14965
15107
|
async init() {
|
|
14966
15108
|
try {
|
|
@@ -15001,10 +15143,10 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
15001
15143
|
const line = this.stdoutBuffer.slice(0, idx).trim();
|
|
15002
15144
|
this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
|
|
15003
15145
|
if (this.pendingResponse) {
|
|
15004
|
-
const { resolve:
|
|
15146
|
+
const { resolve: resolve10, timer } = this.pendingResponse;
|
|
15005
15147
|
this.pendingResponse = null;
|
|
15006
15148
|
clearTimeout(timer);
|
|
15007
|
-
|
|
15149
|
+
resolve10(line);
|
|
15008
15150
|
}
|
|
15009
15151
|
idx = this.stdoutBuffer.indexOf("\n");
|
|
15010
15152
|
}
|
|
@@ -15034,29 +15176,35 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
15034
15176
|
async scanString(content, contentName) {
|
|
15035
15177
|
if (!this.available || !this.process)
|
|
15036
15178
|
return null;
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15179
|
+
return this.enqueueScan(async () => {
|
|
15180
|
+
const process2 = this.process;
|
|
15181
|
+
if (!this.available || !process2)
|
|
15182
|
+
return null;
|
|
15183
|
+
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
15184
|
+
try {
|
|
15185
|
+
const req = JSON.stringify({ content: truncated, contentName });
|
|
15186
|
+
process2.stdin?.write(`${req}
|
|
15041
15187
|
`);
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15188
|
+
const line = await this.waitForLine(PS_TIMEOUT);
|
|
15189
|
+
const amsiResult = parseInt(line, 10);
|
|
15190
|
+
if (Number.isNaN(amsiResult) || amsiResult < 0) {
|
|
15191
|
+
if (!this.invalidResultLogged) {
|
|
15192
|
+
this.invalidResultLogged = true;
|
|
15193
|
+
this.logger.warn("AMSI: PowerShell scan returned invalid result", {
|
|
15194
|
+
stdout: line.slice(0, 100),
|
|
15195
|
+
contentName
|
|
15196
|
+
});
|
|
15197
|
+
}
|
|
15198
|
+
return null;
|
|
15051
15199
|
}
|
|
15200
|
+
this.logger.debug("AMSI: PowerShell scan result", { contentName, amsiResult });
|
|
15201
|
+
return interpretAmsiResult(amsiResult, content, contentName);
|
|
15202
|
+
} catch (e) {
|
|
15203
|
+
this.logger.warn("AMSI: PowerShell scan failed", { error: String(e), contentName });
|
|
15204
|
+
this.close();
|
|
15052
15205
|
return null;
|
|
15053
15206
|
}
|
|
15054
|
-
|
|
15055
|
-
return interpretAmsiResult(amsiResult, content, contentName);
|
|
15056
|
-
} catch (e) {
|
|
15057
|
-
this.logger.warn("AMSI: PowerShell scan failed", { error: String(e), contentName });
|
|
15058
|
-
return null;
|
|
15059
|
-
}
|
|
15207
|
+
});
|
|
15060
15208
|
}
|
|
15061
15209
|
close() {
|
|
15062
15210
|
if (this.process) {
|
|
@@ -15097,7 +15245,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15097
15245
|
if (!this.available)
|
|
15098
15246
|
return null;
|
|
15099
15247
|
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
15100
|
-
return new Promise((
|
|
15248
|
+
return new Promise((resolve10) => {
|
|
15101
15249
|
try {
|
|
15102
15250
|
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
|
|
15103
15251
|
let stdout = "";
|
|
@@ -15107,7 +15255,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15107
15255
|
ps.kill();
|
|
15108
15256
|
} catch {
|
|
15109
15257
|
}
|
|
15110
|
-
|
|
15258
|
+
resolve10(null);
|
|
15111
15259
|
}, PS_TIMEOUT);
|
|
15112
15260
|
ps.stdout?.on("data", (chunk) => {
|
|
15113
15261
|
stdout += chunk.toString();
|
|
@@ -15120,7 +15268,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15120
15268
|
ps.on("error", (err) => {
|
|
15121
15269
|
clearTimeout(timer);
|
|
15122
15270
|
this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
|
|
15123
|
-
|
|
15271
|
+
resolve10(null);
|
|
15124
15272
|
});
|
|
15125
15273
|
ps.on("exit", () => {
|
|
15126
15274
|
clearTimeout(timer);
|
|
@@ -15133,11 +15281,11 @@ var WslPowershellAmsiBackend = class {
|
|
|
15133
15281
|
contentName
|
|
15134
15282
|
});
|
|
15135
15283
|
}
|
|
15136
|
-
|
|
15284
|
+
resolve10(null);
|
|
15137
15285
|
return;
|
|
15138
15286
|
}
|
|
15139
15287
|
this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
|
|
15140
|
-
|
|
15288
|
+
resolve10(interpretAmsiResult(amsiResult, content, contentName));
|
|
15141
15289
|
});
|
|
15142
15290
|
const req = JSON.stringify({ content: truncated, contentName });
|
|
15143
15291
|
ps.stdin?.write(`${req}
|
|
@@ -15145,7 +15293,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15145
15293
|
ps.stdin?.end();
|
|
15146
15294
|
} catch (e) {
|
|
15147
15295
|
this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
|
|
15148
|
-
|
|
15296
|
+
resolve10(null);
|
|
15149
15297
|
}
|
|
15150
15298
|
});
|
|
15151
15299
|
}
|
|
@@ -15291,7 +15439,7 @@ init_file_utils();
|
|
|
15291
15439
|
var import_meta = {};
|
|
15292
15440
|
function resolveVersion() {
|
|
15293
15441
|
if (true)
|
|
15294
|
-
return "0.
|
|
15442
|
+
return "0.12.0";
|
|
15295
15443
|
try {
|
|
15296
15444
|
const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
|
|
15297
15445
|
const pkg = JSON.parse(getFileContentSync(pkgPath));
|
|
@@ -15440,9 +15588,10 @@ var FileCheckClient = class {
|
|
|
15440
15588
|
var import_node_fs = require("node:fs");
|
|
15441
15589
|
var import_node_path6 = require("node:path");
|
|
15442
15590
|
init_config();
|
|
15443
|
-
var MODEL_SCHEMA_VERSION = "
|
|
15591
|
+
var MODEL_SCHEMA_VERSION = "v2";
|
|
15444
15592
|
var REQUIRED_MODELS_BY_SCHEMA = {
|
|
15445
|
-
v1: ["pi-model"]
|
|
15593
|
+
v1: ["pi-model"],
|
|
15594
|
+
v2: ["pi-model"]
|
|
15446
15595
|
};
|
|
15447
15596
|
var REQUIRED_MODEL_FILES = {
|
|
15448
15597
|
"pi-model": [
|
|
@@ -15476,8 +15625,11 @@ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
|
15476
15625
|
init_types2();
|
|
15477
15626
|
var STALE_LOCK_MS = 60 * 60 * 1e3;
|
|
15478
15627
|
|
|
15628
|
+
// ../core/dist/clients/model-manifest.js
|
|
15629
|
+
init_config();
|
|
15630
|
+
|
|
15479
15631
|
// ../core/dist/sage-proxy.js
|
|
15480
|
-
function
|
|
15632
|
+
function mapSageHostOs(platform) {
|
|
15481
15633
|
switch (platform) {
|
|
15482
15634
|
case "win32":
|
|
15483
15635
|
return "WINDOWS";
|
|
@@ -15489,21 +15641,37 @@ function mapSageProxyOs(platform) {
|
|
|
15489
15641
|
return platform;
|
|
15490
15642
|
}
|
|
15491
15643
|
}
|
|
15492
|
-
function
|
|
15644
|
+
function mapSageHostArchitecture(arch) {
|
|
15493
15645
|
return arch.toUpperCase();
|
|
15494
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
|
+
}
|
|
15495
15662
|
function buildSageProxyEnvelope(args) {
|
|
15496
15663
|
return {
|
|
15497
15664
|
identity: { uuid: args.iid },
|
|
15498
15665
|
product: { version_app: args.versionApp },
|
|
15499
15666
|
platform: {
|
|
15500
|
-
os: args.platformOs ??
|
|
15501
|
-
architecture: args.platformArchitecture ??
|
|
15667
|
+
os: args.platformOs ?? mapSageHostOs(process.platform),
|
|
15668
|
+
architecture: args.platformArchitecture ?? mapSageHostArchitecture(process.arch)
|
|
15502
15669
|
},
|
|
15503
15670
|
agent: {
|
|
15504
15671
|
agent_runtime: args.agentRuntime,
|
|
15505
15672
|
agent_runtime_version: args.agentRuntimeVersion
|
|
15506
|
-
}
|
|
15673
|
+
},
|
|
15674
|
+
...args.config ? { config: buildSageUserConfig(args.config) } : {}
|
|
15507
15675
|
};
|
|
15508
15676
|
}
|
|
15509
15677
|
|
|
@@ -15937,6 +16105,8 @@ ${err.stack}` : String(err);
|
|
|
15937
16105
|
*/
|
|
15938
16106
|
async classifyChunk(text) {
|
|
15939
16107
|
const tok = this.tokenizer;
|
|
16108
|
+
if (!tok)
|
|
16109
|
+
throw new Error("PI tokenizer is not loaded");
|
|
15940
16110
|
const encoded = tok.call(text, {
|
|
15941
16111
|
truncation: true,
|
|
15942
16112
|
max_length: MAX_TOKENS,
|
|
@@ -15969,6 +16139,8 @@ ${err.stack}` : String(err);
|
|
|
15969
16139
|
*/
|
|
15970
16140
|
chunkText(text) {
|
|
15971
16141
|
const tok = this.tokenizer;
|
|
16142
|
+
if (!tok)
|
|
16143
|
+
throw new Error("PI tokenizer is not loaded");
|
|
15972
16144
|
const encoded = tok.tokenize(text, {
|
|
15973
16145
|
addSpecialTokens: false,
|
|
15974
16146
|
returnOffsetMapping: true,
|
|
@@ -15976,6 +16148,8 @@ ${err.stack}` : String(err);
|
|
|
15976
16148
|
});
|
|
15977
16149
|
const tokenIds = encoded.input_ids;
|
|
15978
16150
|
const offsets = encoded.offset_mapping;
|
|
16151
|
+
if (!offsets)
|
|
16152
|
+
return [text];
|
|
15979
16153
|
if (tokenIds.length <= MAX_TOKENS)
|
|
15980
16154
|
return [text];
|
|
15981
16155
|
const chunks = [];
|
|
@@ -16005,6 +16179,9 @@ ${content.slice(-tailLen)}`;
|
|
|
16005
16179
|
// ../core/dist/index.js
|
|
16006
16180
|
init_pi_deps_installer();
|
|
16007
16181
|
|
|
16182
|
+
// ../core/dist/clients/skill-analyze.js
|
|
16183
|
+
init_types2();
|
|
16184
|
+
|
|
16008
16185
|
// ../core/dist/clients/skill-check.js
|
|
16009
16186
|
init_types2();
|
|
16010
16187
|
var DEFAULT_TIMEOUT4 = 5;
|
|
@@ -16027,9 +16204,13 @@ var SkillCheckClient = class {
|
|
|
16027
16204
|
/**
|
|
16028
16205
|
* Check a list of skill IDs against the proxy.
|
|
16029
16206
|
*
|
|
16030
|
-
* Returns a Map keyed by skill id
|
|
16031
|
-
*
|
|
16032
|
-
*
|
|
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.
|
|
16033
16214
|
*
|
|
16034
16215
|
* Fails-open: on any error returns an empty Map. Per-batch errors do
|
|
16035
16216
|
* not poison results from successful batches.
|
|
@@ -16073,8 +16254,10 @@ var SkillCheckClient = class {
|
|
|
16073
16254
|
const data = await response.json();
|
|
16074
16255
|
const results = data.results ?? {};
|
|
16075
16256
|
for (const id of skillIds) {
|
|
16076
|
-
if (!(id in results))
|
|
16257
|
+
if (!(id in results)) {
|
|
16258
|
+
out.set(id, null);
|
|
16077
16259
|
continue;
|
|
16260
|
+
}
|
|
16078
16261
|
const raw = results[id];
|
|
16079
16262
|
if (raw === null || raw === void 0) {
|
|
16080
16263
|
out.set(id, null);
|
|
@@ -16089,17 +16272,10 @@ var SkillCheckClient = class {
|
|
|
16089
16272
|
return out;
|
|
16090
16273
|
}
|
|
16091
16274
|
parseResult(skillId, raw) {
|
|
16092
|
-
const recommendationsRaw = raw.recommendations ?? [];
|
|
16093
|
-
const recommendations = recommendationsRaw.filter((r) => typeof r === "string");
|
|
16094
|
-
const categoriesRaw = raw.threat_categories ?? [];
|
|
16095
|
-
const threatCategories = categoriesRaw.filter((c) => typeof c === "string");
|
|
16096
16275
|
return {
|
|
16097
16276
|
skillId,
|
|
16098
16277
|
verdict: typeof raw.verdict === "string" ? raw.verdict : void 0,
|
|
16099
|
-
|
|
16100
|
-
summary: typeof raw.summary === "string" ? raw.summary : void 0,
|
|
16101
|
-
recommendations,
|
|
16102
|
-
threatCategories
|
|
16278
|
+
summary: typeof raw.summary === "string" ? raw.summary : void 0
|
|
16103
16279
|
};
|
|
16104
16280
|
}
|
|
16105
16281
|
};
|
|
@@ -16107,6 +16283,70 @@ var SkillCheckClient = class {
|
|
|
16107
16283
|
// ../core/dist/index.js
|
|
16108
16284
|
init_config();
|
|
16109
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
|
+
|
|
16110
16350
|
// ../core/dist/config-diagnostics.js
|
|
16111
16351
|
init_config();
|
|
16112
16352
|
init_file_utils();
|
|
@@ -16243,184 +16483,10 @@ function isScannableContent(content) {
|
|
|
16243
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);
|
|
16244
16484
|
}
|
|
16245
16485
|
|
|
16246
|
-
// ../core/dist/content-snapshot.js
|
|
16247
|
-
var import_node_os2 = require("node:os");
|
|
16248
|
-
var CONTENT_FIELD_LIMITS = Object.freeze({
|
|
16249
|
-
command: 512,
|
|
16250
|
-
url: 512,
|
|
16251
|
-
file_path: 512,
|
|
16252
|
-
package_name: 256,
|
|
16253
|
-
package_version: 128,
|
|
16254
|
-
package_registry: 128
|
|
16255
|
-
});
|
|
16256
|
-
function safeTruncate(value, maxLen) {
|
|
16257
|
-
if (maxLen <= 0)
|
|
16258
|
-
return "";
|
|
16259
|
-
if (value.length <= maxLen)
|
|
16260
|
-
return value;
|
|
16261
|
-
const cutIndex = maxLen;
|
|
16262
|
-
const codeUnit = value.charCodeAt(cutIndex - 1);
|
|
16263
|
-
if (codeUnit >= 55296 && codeUnit <= 56319) {
|
|
16264
|
-
return value.slice(0, cutIndex - 1);
|
|
16265
|
-
}
|
|
16266
|
-
return value.slice(0, cutIndex);
|
|
16267
|
-
}
|
|
16268
|
-
function scrubHomePath(value) {
|
|
16269
|
-
const home = (0, import_node_os2.homedir)();
|
|
16270
|
-
if (!home)
|
|
16271
|
-
return value;
|
|
16272
|
-
const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
16273
|
-
if (!normalizedHome)
|
|
16274
|
-
return value;
|
|
16275
|
-
const normalizedValue = value.replace(/\\/g, "/");
|
|
16276
|
-
if (normalizedValue === normalizedHome)
|
|
16277
|
-
return "~";
|
|
16278
|
-
if (normalizedValue.startsWith(`${normalizedHome}/`)) {
|
|
16279
|
-
return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
|
|
16280
|
-
}
|
|
16281
|
-
return value;
|
|
16282
|
-
}
|
|
16283
|
-
function asString(v) {
|
|
16284
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
16285
|
-
}
|
|
16286
|
-
function resolveFilePath(toolInput) {
|
|
16287
|
-
return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
|
|
16288
|
-
}
|
|
16289
|
-
function resolveWebFetchUrl(toolInput) {
|
|
16290
|
-
const single = asString(toolInput.url);
|
|
16291
|
-
if (single)
|
|
16292
|
-
return single;
|
|
16293
|
-
const urls = toolInput.urls;
|
|
16294
|
-
if (Array.isArray(urls)) {
|
|
16295
|
-
for (const u of urls) {
|
|
16296
|
-
if (typeof u === "string" && u.length > 0)
|
|
16297
|
-
return u;
|
|
16298
|
-
}
|
|
16299
|
-
}
|
|
16300
|
-
return void 0;
|
|
16301
|
-
}
|
|
16302
|
-
function extractFirstApplyPatchPath(patchText) {
|
|
16303
|
-
if (!patchText)
|
|
16304
|
-
return void 0;
|
|
16305
|
-
const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
|
|
16306
|
-
if (headerMatch?.[1]) {
|
|
16307
|
-
const trimmed = headerMatch[1].trim();
|
|
16308
|
-
if (trimmed)
|
|
16309
|
-
return trimmed;
|
|
16310
|
-
}
|
|
16311
|
-
const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
|
|
16312
|
-
if (renameMatch?.[1]) {
|
|
16313
|
-
const raw = renameMatch[1].trim();
|
|
16314
|
-
const arrow = raw.indexOf(" -> ");
|
|
16315
|
-
if (arrow !== -1) {
|
|
16316
|
-
const src = raw.slice(0, arrow).trim();
|
|
16317
|
-
if (src)
|
|
16318
|
-
return src;
|
|
16319
|
-
const dst = raw.slice(arrow + 4).trim();
|
|
16320
|
-
if (dst)
|
|
16321
|
-
return dst;
|
|
16322
|
-
} else if (raw) {
|
|
16323
|
-
return raw;
|
|
16324
|
-
}
|
|
16325
|
-
}
|
|
16326
|
-
return void 0;
|
|
16327
|
-
}
|
|
16328
|
-
function extractMcpContent(toolInput) {
|
|
16329
|
-
const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
|
|
16330
|
-
const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
|
|
16331
|
-
return {
|
|
16332
|
-
command: asString(candidate.command),
|
|
16333
|
-
url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
|
|
16334
|
-
file_path: resolveFilePath(candidate)
|
|
16335
|
-
};
|
|
16336
|
-
}
|
|
16337
|
-
function firstUrlArtifact(artifacts) {
|
|
16338
|
-
for (const a of artifacts) {
|
|
16339
|
-
if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
|
|
16340
|
-
return a.value;
|
|
16341
|
-
}
|
|
16342
|
-
}
|
|
16343
|
-
return void 0;
|
|
16344
|
-
}
|
|
16345
|
-
function applyFieldLimits(content) {
|
|
16346
|
-
for (const [key, value] of Object.entries(content)) {
|
|
16347
|
-
if (typeof value !== "string")
|
|
16348
|
-
continue;
|
|
16349
|
-
let sanitized = value;
|
|
16350
|
-
if (key === "file_path" || key === "command") {
|
|
16351
|
-
sanitized = scrubHomePath(sanitized);
|
|
16352
|
-
}
|
|
16353
|
-
const limit = CONTENT_FIELD_LIMITS[key];
|
|
16354
|
-
if (limit && sanitized.length > limit) {
|
|
16355
|
-
sanitized = safeTruncate(sanitized, limit);
|
|
16356
|
-
}
|
|
16357
|
-
content[key] = sanitized;
|
|
16358
|
-
}
|
|
16359
|
-
}
|
|
16360
|
-
function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
|
|
16361
|
-
const content = {};
|
|
16362
|
-
switch (toolType) {
|
|
16363
|
-
case "Bash": {
|
|
16364
|
-
const command = asString(toolInput.command);
|
|
16365
|
-
if (command)
|
|
16366
|
-
content.command = command;
|
|
16367
|
-
break;
|
|
16368
|
-
}
|
|
16369
|
-
case "WebFetch": {
|
|
16370
|
-
const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
|
|
16371
|
-
if (url)
|
|
16372
|
-
content.url = url;
|
|
16373
|
-
break;
|
|
16374
|
-
}
|
|
16375
|
-
case "Write":
|
|
16376
|
-
case "Edit":
|
|
16377
|
-
case "Read":
|
|
16378
|
-
case "Delete": {
|
|
16379
|
-
const filePath = resolveFilePath(toolInput);
|
|
16380
|
-
if (filePath)
|
|
16381
|
-
content.file_path = filePath;
|
|
16382
|
-
break;
|
|
16383
|
-
}
|
|
16384
|
-
case "ApplyPatch": {
|
|
16385
|
-
const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
|
|
16386
|
-
const filePath = extractFirstApplyPatchPath(patchText);
|
|
16387
|
-
if (filePath)
|
|
16388
|
-
content.file_path = filePath;
|
|
16389
|
-
break;
|
|
16390
|
-
}
|
|
16391
|
-
case "MCP": {
|
|
16392
|
-
const mcp = extractMcpContent(toolInput);
|
|
16393
|
-
if (mcp.command)
|
|
16394
|
-
content.command = mcp.command;
|
|
16395
|
-
if (mcp.url)
|
|
16396
|
-
content.url = mcp.url;
|
|
16397
|
-
if (mcp.file_path)
|
|
16398
|
-
content.file_path = mcp.file_path;
|
|
16399
|
-
break;
|
|
16400
|
-
}
|
|
16401
|
-
}
|
|
16402
|
-
if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
|
|
16403
|
-
const firstUrl = signals.url_checks[0]?.url;
|
|
16404
|
-
if (firstUrl)
|
|
16405
|
-
content.url = firstUrl;
|
|
16406
|
-
}
|
|
16407
|
-
if (signals.package_checks && signals.package_checks.length > 0) {
|
|
16408
|
-
const p = signals.package_checks[0];
|
|
16409
|
-
if (p?.package_name)
|
|
16410
|
-
content.package_name = p.package_name;
|
|
16411
|
-
if (p?.package_version)
|
|
16412
|
-
content.package_version = p.package_version;
|
|
16413
|
-
if (p?.package_registry)
|
|
16414
|
-
content.package_registry = p.package_registry;
|
|
16415
|
-
}
|
|
16416
|
-
applyFieldLimits(content);
|
|
16417
|
-
return content;
|
|
16418
|
-
}
|
|
16419
|
-
|
|
16420
16486
|
// ../core/dist/extended-info.js
|
|
16421
|
-
var
|
|
16487
|
+
var import_promises4 = require("node:fs/promises");
|
|
16422
16488
|
var import_node_os3 = require("node:os");
|
|
16423
|
-
var
|
|
16489
|
+
var import_node_path11 = require("node:path");
|
|
16424
16490
|
init_file_utils();
|
|
16425
16491
|
init_types2();
|
|
16426
16492
|
var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
|
|
@@ -16486,8 +16552,8 @@ function sanitizeDocument(parsed, logger) {
|
|
|
16486
16552
|
return out;
|
|
16487
16553
|
}
|
|
16488
16554
|
async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
16489
|
-
const sageDir = sageDirPath ?? (0,
|
|
16490
|
-
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);
|
|
16491
16557
|
const cached = cache.get(filePath);
|
|
16492
16558
|
if (cached)
|
|
16493
16559
|
return cached.value;
|
|
@@ -16498,7 +16564,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
|
16498
16564
|
async function loadExtendedInfoUncached(filePath, logger) {
|
|
16499
16565
|
let size;
|
|
16500
16566
|
try {
|
|
16501
|
-
const info = await (0,
|
|
16567
|
+
const info = await (0, import_promises4.stat)(filePath);
|
|
16502
16568
|
if (!info.isFile()) {
|
|
16503
16569
|
logger.debug(`extended-info: not a regular file at ${filePath}`);
|
|
16504
16570
|
return null;
|
|
@@ -16585,14 +16651,14 @@ function mergeExtendedInfo(envelope, extendedInfo) {
|
|
|
16585
16651
|
}
|
|
16586
16652
|
|
|
16587
16653
|
// ../core/dist/installation-id.js
|
|
16588
|
-
var
|
|
16589
|
-
var
|
|
16590
|
-
var
|
|
16654
|
+
var import_node_crypto4 = require("node:crypto");
|
|
16655
|
+
var import_promises5 = require("node:fs/promises");
|
|
16656
|
+
var import_node_path12 = require("node:path");
|
|
16591
16657
|
init_config();
|
|
16592
16658
|
init_file_utils();
|
|
16593
16659
|
async function getInstallationId(sageDirPath) {
|
|
16594
16660
|
const sageDir = sageDirPath ?? resolvePath("~/.sage");
|
|
16595
|
-
const idPath = (0,
|
|
16661
|
+
const idPath = (0, import_node_path12.join)(sageDir, "installation-id");
|
|
16596
16662
|
let fileExists = false;
|
|
16597
16663
|
try {
|
|
16598
16664
|
const existing = await getFileContent(idPath, "utf-8");
|
|
@@ -16603,9 +16669,9 @@ async function getInstallationId(sageDirPath) {
|
|
|
16603
16669
|
} catch {
|
|
16604
16670
|
}
|
|
16605
16671
|
try {
|
|
16606
|
-
const id = (0,
|
|
16607
|
-
await (0,
|
|
16608
|
-
await (0,
|
|
16672
|
+
const id = (0, import_node_crypto4.randomUUID)();
|
|
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" });
|
|
16609
16675
|
return id;
|
|
16610
16676
|
} catch (err) {
|
|
16611
16677
|
if (err.code === "EEXIST") {
|
|
@@ -16634,10 +16700,10 @@ function resolveTimeoutMs() {
|
|
|
16634
16700
|
const ms = Math.floor(parsed * 1e3);
|
|
16635
16701
|
return Math.min(ms, MAX_EFFECTIVE_TIMEOUT_MS);
|
|
16636
16702
|
}
|
|
16637
|
-
async function
|
|
16703
|
+
async function sendCommunityIqTelemetry(args) {
|
|
16638
16704
|
const logger = args.logger ?? nullLogger;
|
|
16639
16705
|
if (!args.communityIqEnabled) {
|
|
16640
|
-
logger.debug("Community IQ disabled, skipping
|
|
16706
|
+
logger.debug("Community IQ disabled, skipping telemetry");
|
|
16641
16707
|
return;
|
|
16642
16708
|
}
|
|
16643
16709
|
let iid;
|
|
@@ -16646,15 +16712,17 @@ async function sendCommunityIqDetection(args) {
|
|
|
16646
16712
|
} catch {
|
|
16647
16713
|
}
|
|
16648
16714
|
if (!iid) {
|
|
16649
|
-
logger.debug("Skipping
|
|
16715
|
+
logger.debug("Skipping telemetry: missing installation id");
|
|
16650
16716
|
return;
|
|
16651
16717
|
}
|
|
16652
16718
|
const envelope = buildSageProxyEnvelope({
|
|
16653
16719
|
iid,
|
|
16654
16720
|
versionApp: VERSION,
|
|
16655
16721
|
agentRuntime: args.agentRuntime ?? "unknown",
|
|
16656
|
-
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
|
|
16657
16724
|
});
|
|
16725
|
+
const blocking = args.blocking ?? true;
|
|
16658
16726
|
const payload = {
|
|
16659
16727
|
...envelope,
|
|
16660
16728
|
block_event: {
|
|
@@ -16663,8 +16731,8 @@ async function sendCommunityIqDetection(args) {
|
|
|
16663
16731
|
// canonicalize before calling `evaluateToolCall`, so no further
|
|
16664
16732
|
// mapping is needed here.
|
|
16665
16733
|
tool_type: args.toolName,
|
|
16666
|
-
verdict: "deny",
|
|
16667
|
-
user_action: "blocked",
|
|
16734
|
+
verdict: blocking ? "deny" : "suspicious",
|
|
16735
|
+
...blocking ? { user_action: "blocked" } : {},
|
|
16668
16736
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16669
16737
|
...args.signals && Object.keys(args.signals).length > 0 ? { signals: args.signals } : {},
|
|
16670
16738
|
content: args.content ?? {}
|
|
@@ -16675,20 +16743,21 @@ async function sendCommunityIqDetection(args) {
|
|
|
16675
16743
|
const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
|
|
16676
16744
|
const enriched = mergeExtendedInfo(payload, extendedInfo);
|
|
16677
16745
|
const timeoutMs = resolveTimeoutMs();
|
|
16746
|
+
const endpoint = blocking ? "/v2/detection" : "/v2/heuristic";
|
|
16678
16747
|
try {
|
|
16679
|
-
const response = await fetch(resolveEndpoint(
|
|
16748
|
+
const response = await fetch(resolveEndpoint(endpoint), {
|
|
16680
16749
|
method: "POST",
|
|
16681
16750
|
headers: { "Content-Type": "application/json" },
|
|
16682
16751
|
body: JSON.stringify(enriched),
|
|
16683
16752
|
signal: AbortSignal.timeout(timeoutMs)
|
|
16684
16753
|
});
|
|
16685
16754
|
if (!response.ok) {
|
|
16686
|
-
logger.warn("
|
|
16755
|
+
logger.warn("Community IQ telemetry send failed", {
|
|
16687
16756
|
eventId: args.eventId,
|
|
16688
16757
|
status: response.status
|
|
16689
16758
|
});
|
|
16690
16759
|
} else {
|
|
16691
|
-
logger.debug("
|
|
16760
|
+
logger.debug("Community IQ telemetry sent", {
|
|
16692
16761
|
eventId: args.eventId,
|
|
16693
16762
|
toolName: args.toolName,
|
|
16694
16763
|
hookType: args.hookType ?? "PreToolUse"
|
|
@@ -16702,30 +16771,50 @@ async function sendCommunityIqDetection(args) {
|
|
|
16702
16771
|
}
|
|
16703
16772
|
}
|
|
16704
16773
|
|
|
16705
|
-
// ../core/dist/
|
|
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
|
+
|
|
16797
|
+
// ../core/dist/policy.js
|
|
16706
16798
|
init_types2();
|
|
16707
|
-
var
|
|
16708
|
-
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
relaxed: 0.95
|
|
16712
|
-
};
|
|
16713
|
-
var SEVERITY_MAP = {
|
|
16714
|
-
critical: "critical",
|
|
16715
|
-
high: "warning",
|
|
16716
|
-
medium: "warning",
|
|
16717
|
-
low: "info"
|
|
16718
|
-
};
|
|
16719
|
-
var ACTION_MAP = {
|
|
16720
|
-
block: "deny",
|
|
16721
|
-
require_approval: "ask",
|
|
16722
|
-
log: "allow"
|
|
16723
|
-
};
|
|
16724
|
-
var DECISION_PRIORITY = {
|
|
16725
|
-
allow: 0,
|
|
16726
|
-
ask: 1,
|
|
16727
|
-
deny: 2
|
|
16799
|
+
var SENSITIVITY_POLICY = {
|
|
16800
|
+
paranoid: { denyThreshold: 0.7, askThreshold: 0.3 },
|
|
16801
|
+
balanced: { denyThreshold: 0.85, askThreshold: 0.5 },
|
|
16802
|
+
relaxed: { denyThreshold: 0.95, askThreshold: 0.7 }
|
|
16728
16803
|
};
|
|
16804
|
+
function applyPolicy(confidence, denyThreshold, askThreshold, logger = nullLogger) {
|
|
16805
|
+
if (!Number.isFinite(confidence) || confidence <= 0 || confidence > 1) {
|
|
16806
|
+
logger.warn("Invalid confidence; treating as allow", { confidence });
|
|
16807
|
+
return "allow";
|
|
16808
|
+
}
|
|
16809
|
+
if (confidence >= denyThreshold)
|
|
16810
|
+
return "deny";
|
|
16811
|
+
if (confidence >= askThreshold)
|
|
16812
|
+
return "ask";
|
|
16813
|
+
return "allow";
|
|
16814
|
+
}
|
|
16815
|
+
|
|
16816
|
+
// ../core/dist/engine.js
|
|
16817
|
+
init_types2();
|
|
16729
16818
|
var PI_LABEL_ONLY_SUFFIXES = /* @__PURE__ */ new Set([
|
|
16730
16819
|
"command",
|
|
16731
16820
|
"stdout",
|
|
@@ -16756,29 +16845,25 @@ function formatPiFinding(result) {
|
|
|
16756
16845
|
return `${where} (${score})`;
|
|
16757
16846
|
}
|
|
16758
16847
|
var DecisionEngine = class {
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16848
|
+
denyThreshold;
|
|
16849
|
+
askThreshold;
|
|
16850
|
+
logger;
|
|
16851
|
+
constructor(sensitivity = "balanced", logger = nullLogger) {
|
|
16852
|
+
const policy = SENSITIVITY_POLICY[sensitivity] ?? SENSITIVITY_POLICY.balanced;
|
|
16853
|
+
this.denyThreshold = policy.denyThreshold;
|
|
16854
|
+
this.askThreshold = policy.askThreshold;
|
|
16855
|
+
this.logger = logger;
|
|
16764
16856
|
}
|
|
16765
16857
|
async decide(sources) {
|
|
16766
16858
|
const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds);
|
|
16767
16859
|
if (signals.length === 0) {
|
|
16768
16860
|
return this.allowVerdict();
|
|
16769
16861
|
}
|
|
16770
|
-
signals.
|
|
16771
|
-
const
|
|
16772
|
-
|
|
16773
|
-
return this.allowVerdict();
|
|
16774
|
-
}
|
|
16862
|
+
const top = signals.reduce((best, s) => s.confidence > best.confidence ? s : best);
|
|
16863
|
+
const maxConfidence = top.confidence;
|
|
16864
|
+
const decision = applyPolicy(maxConfidence, this.denyThreshold, this.askThreshold, this.logger);
|
|
16775
16865
|
const allArtifacts = [...new Map(signals.map((s) => [s.artifact, s.artifact])).values()];
|
|
16776
16866
|
const allReasons = [...new Map(signals.map((s) => [s.reason, s.reason])).values()];
|
|
16777
|
-
const maxConfidence = Math.max(...signals.map((s) => s.confidence));
|
|
16778
|
-
let decision = top.decision;
|
|
16779
|
-
if (decision === "deny" && maxConfidence < this.threshold && top.source !== "pi_check") {
|
|
16780
|
-
decision = "ask";
|
|
16781
|
-
}
|
|
16782
16867
|
return {
|
|
16783
16868
|
decision,
|
|
16784
16869
|
category: top.category,
|
|
@@ -16793,24 +16878,26 @@ var DecisionEngine = class {
|
|
|
16793
16878
|
collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults, piCheckResults, piThresholds) {
|
|
16794
16879
|
const signals = [];
|
|
16795
16880
|
for (const match of heuristicMatches) {
|
|
16881
|
+
const { confidence, category, severity, id, title } = match.threat;
|
|
16882
|
+
if (applyPolicy(confidence, this.denyThreshold, this.askThreshold, this.logger) === "allow")
|
|
16883
|
+
continue;
|
|
16796
16884
|
signals.push({
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
severity: SEVERITY_MAP[match.threat.severity] ?? "warning",
|
|
16885
|
+
category,
|
|
16886
|
+
confidence,
|
|
16887
|
+
severity,
|
|
16801
16888
|
source: "heuristic",
|
|
16802
|
-
threatId:
|
|
16803
|
-
reason:
|
|
16889
|
+
threatId: id,
|
|
16890
|
+
reason: title,
|
|
16804
16891
|
artifact: match.artifact
|
|
16805
16892
|
});
|
|
16806
16893
|
}
|
|
16807
16894
|
for (const result of urlCheckResults) {
|
|
16808
16895
|
if (result.isMalicious) {
|
|
16896
|
+
const confidence = 1;
|
|
16809
16897
|
const findingDetails = result.findings.map((f) => `${f.severityName}/${f.typeName}`).join(", ");
|
|
16810
16898
|
signals.push({
|
|
16811
|
-
decision: "deny",
|
|
16812
16899
|
category: "network_egress",
|
|
16813
|
-
confidence
|
|
16900
|
+
confidence,
|
|
16814
16901
|
severity: "critical",
|
|
16815
16902
|
source: "url_check",
|
|
16816
16903
|
threatId: null,
|
|
@@ -16832,7 +16919,6 @@ var DecisionEngine = class {
|
|
|
16832
16919
|
for (const result of amsiCheckResults) {
|
|
16833
16920
|
if (result.isDetected) {
|
|
16834
16921
|
signals.push({
|
|
16835
|
-
decision: "deny",
|
|
16836
16922
|
category: "malware",
|
|
16837
16923
|
confidence: 1,
|
|
16838
16924
|
severity: "critical",
|
|
@@ -16843,7 +16929,6 @@ var DecisionEngine = class {
|
|
|
16843
16929
|
});
|
|
16844
16930
|
} else if (result.isBlockedByAdmin) {
|
|
16845
16931
|
signals.push({
|
|
16846
|
-
decision: "deny",
|
|
16847
16932
|
category: "malware",
|
|
16848
16933
|
confidence: 0.9,
|
|
16849
16934
|
severity: "critical",
|
|
@@ -16858,24 +16943,21 @@ var DecisionEngine = class {
|
|
|
16858
16943
|
if (piCheckResults) {
|
|
16859
16944
|
const highRisk = piThresholds?.highRisk ?? DEFAULT_PI_HIGH_RISK_THRESHOLD;
|
|
16860
16945
|
const mediumRisk = piThresholds?.mediumRisk ?? DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
|
|
16861
|
-
const suppressMediumPi = this.sensitivity === "relaxed";
|
|
16862
16946
|
for (const result of piCheckResults) {
|
|
16863
16947
|
if (result.risk >= highRisk) {
|
|
16864
16948
|
signals.push({
|
|
16865
|
-
decision: "deny",
|
|
16866
16949
|
category: "prompt_injection",
|
|
16867
|
-
confidence:
|
|
16950
|
+
confidence: 1,
|
|
16868
16951
|
severity: "critical",
|
|
16869
16952
|
source: "pi_check",
|
|
16870
16953
|
threatId: "PROMPT_INJECTION",
|
|
16871
16954
|
reason: buildPiReason("Prompt injection detected", result),
|
|
16872
16955
|
artifact: result.contentName
|
|
16873
16956
|
});
|
|
16874
|
-
} else if (
|
|
16957
|
+
} else if (result.risk >= mediumRisk) {
|
|
16875
16958
|
signals.push({
|
|
16876
|
-
decision: "ask",
|
|
16877
16959
|
category: "prompt_injection",
|
|
16878
|
-
confidence:
|
|
16960
|
+
confidence: 0.6,
|
|
16879
16961
|
severity: "warning",
|
|
16880
16962
|
source: "pi_check",
|
|
16881
16963
|
threatId: "PROMPT_INJECTION",
|
|
@@ -16890,21 +16972,10 @@ var DecisionEngine = class {
|
|
|
16890
16972
|
packageVerdictToSignal(pkg) {
|
|
16891
16973
|
switch (pkg.verdict) {
|
|
16892
16974
|
case "not_found":
|
|
16893
|
-
return {
|
|
16894
|
-
decision: "deny",
|
|
16895
|
-
category: "supply_chain",
|
|
16896
|
-
confidence: 0.95,
|
|
16897
|
-
severity: "critical",
|
|
16898
|
-
source: "package_check",
|
|
16899
|
-
threatId: null,
|
|
16900
|
-
reason: pkg.details,
|
|
16901
|
-
artifact: pkg.packageName
|
|
16902
|
-
};
|
|
16903
16975
|
case "malicious":
|
|
16904
16976
|
return {
|
|
16905
|
-
decision: "deny",
|
|
16906
16977
|
category: "supply_chain",
|
|
16907
|
-
confidence:
|
|
16978
|
+
confidence: pkg.confidence,
|
|
16908
16979
|
severity: "critical",
|
|
16909
16980
|
source: "package_check",
|
|
16910
16981
|
threatId: null,
|
|
@@ -16912,21 +16983,10 @@ var DecisionEngine = class {
|
|
|
16912
16983
|
artifact: pkg.packageName
|
|
16913
16984
|
};
|
|
16914
16985
|
case "suspicious_age":
|
|
16915
|
-
return {
|
|
16916
|
-
decision: "ask",
|
|
16917
|
-
category: "supply_chain",
|
|
16918
|
-
confidence: 0.75,
|
|
16919
|
-
severity: "warning",
|
|
16920
|
-
source: "package_check",
|
|
16921
|
-
threatId: null,
|
|
16922
|
-
reason: pkg.details,
|
|
16923
|
-
artifact: pkg.packageName
|
|
16924
|
-
};
|
|
16925
16986
|
case "unknown":
|
|
16926
16987
|
return {
|
|
16927
|
-
decision: "ask",
|
|
16928
16988
|
category: "supply_chain",
|
|
16929
|
-
confidence:
|
|
16989
|
+
confidence: pkg.confidence,
|
|
16930
16990
|
severity: "warning",
|
|
16931
16991
|
source: "package_check",
|
|
16932
16992
|
threatId: null,
|
|
@@ -16941,7 +17001,7 @@ var DecisionEngine = class {
|
|
|
16941
17001
|
return {
|
|
16942
17002
|
decision: "allow",
|
|
16943
17003
|
category: "none",
|
|
16944
|
-
confidence:
|
|
17004
|
+
confidence: 0,
|
|
16945
17005
|
severity: "info",
|
|
16946
17006
|
source: "none",
|
|
16947
17007
|
artifacts: [],
|
|
@@ -16952,32 +17012,32 @@ var DecisionEngine = class {
|
|
|
16952
17012
|
};
|
|
16953
17013
|
|
|
16954
17014
|
// ../core/dist/evaluator.js
|
|
16955
|
-
var
|
|
17015
|
+
var import_node_crypto6 = require("node:crypto");
|
|
16956
17016
|
init_config();
|
|
16957
17017
|
|
|
16958
17018
|
// ../core/dist/exceptions.js
|
|
16959
|
-
var
|
|
16960
|
-
var
|
|
17019
|
+
var import_node_crypto5 = require("node:crypto");
|
|
17020
|
+
var import_node_path15 = require("node:path");
|
|
16961
17021
|
init_config();
|
|
16962
17022
|
init_file_utils();
|
|
16963
17023
|
|
|
16964
17024
|
// ../core/dist/trusted-domains.js
|
|
16965
|
-
var
|
|
16966
|
-
var
|
|
17025
|
+
var import_promises6 = require("node:fs/promises");
|
|
17026
|
+
var import_node_path14 = require("node:path");
|
|
16967
17027
|
var import_yaml = __toESM(require_dist(), 1);
|
|
16968
17028
|
init_file_utils();
|
|
16969
17029
|
init_types2();
|
|
16970
|
-
async function loadTrustedDomains(
|
|
17030
|
+
async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
|
|
16971
17031
|
let files;
|
|
16972
17032
|
try {
|
|
16973
|
-
files = (await (0,
|
|
17033
|
+
files = (await (0, import_promises6.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
16974
17034
|
} catch {
|
|
16975
|
-
logger.debug("
|
|
17035
|
+
logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
|
|
16976
17036
|
return [];
|
|
16977
17037
|
}
|
|
16978
17038
|
const domains = [];
|
|
16979
17039
|
for (const filename of files) {
|
|
16980
|
-
const filePath = (0,
|
|
17040
|
+
const filePath = (0, import_node_path14.join)(trustedDomainsDir, filename);
|
|
16981
17041
|
let content;
|
|
16982
17042
|
try {
|
|
16983
17043
|
content = await getFileContent(filePath);
|
|
@@ -17015,7 +17075,7 @@ async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
|
|
|
17015
17075
|
domains.push({ domain: domain.toLowerCase(), reason });
|
|
17016
17076
|
}
|
|
17017
17077
|
}
|
|
17018
|
-
logger.debug(`Loaded ${domains.length} trusted domains from ${
|
|
17078
|
+
logger.debug(`Loaded ${domains.length} trusted domains from ${trustedDomainsDir}`);
|
|
17019
17079
|
return domains;
|
|
17020
17080
|
}
|
|
17021
17081
|
function extractDomain(url) {
|
|
@@ -17042,7 +17102,7 @@ init_types2();
|
|
|
17042
17102
|
var MAX_RULES_WARNING = 100;
|
|
17043
17103
|
var REGEX_TIMEOUT_MS = 50;
|
|
17044
17104
|
function computeRuleId(decision, match, pattern) {
|
|
17045
|
-
const hash = (0,
|
|
17105
|
+
const hash = (0, import_node_crypto5.createHash)("sha256").update(`${decision}:${match}:${pattern}`).digest("hex");
|
|
17046
17106
|
return hash.slice(0, 8);
|
|
17047
17107
|
}
|
|
17048
17108
|
async function loadExceptions(config2, logger = nullLogger) {
|
|
@@ -17253,7 +17313,7 @@ function matchesDomain(pattern, url) {
|
|
|
17253
17313
|
function normalizePatternPath(pattern) {
|
|
17254
17314
|
const home = getHomeDir();
|
|
17255
17315
|
const expanded = pattern.startsWith("~/") || pattern === "~" ? `${home}${pattern.slice(1)}` : pattern;
|
|
17256
|
-
return (0,
|
|
17316
|
+
return (0, import_node_path15.normalize)(expanded);
|
|
17257
17317
|
}
|
|
17258
17318
|
function matchesPath(pattern, filePath) {
|
|
17259
17319
|
const hasWildcard = pattern.includes("*");
|
|
@@ -17264,7 +17324,7 @@ function matchesPath(pattern, filePath) {
|
|
|
17264
17324
|
const normalizedPath = normalizePatternPath(filePath);
|
|
17265
17325
|
if (normalizedPath === normalizedPattern)
|
|
17266
17326
|
return true;
|
|
17267
|
-
if (normalizedPath.startsWith(normalizedPattern +
|
|
17327
|
+
if (normalizedPath.startsWith(normalizedPattern + import_node_path15.sep))
|
|
17268
17328
|
return true;
|
|
17269
17329
|
return false;
|
|
17270
17330
|
}
|
|
@@ -17620,7 +17680,7 @@ var PackageChecker = class {
|
|
|
17620
17680
|
packageName: pkg.name,
|
|
17621
17681
|
registry: pkg.registry,
|
|
17622
17682
|
verdict: "not_found",
|
|
17623
|
-
confidence: 0.
|
|
17683
|
+
confidence: 0.95,
|
|
17624
17684
|
details: `Package '${pkg.name}@${pkg.version}' version not found on ${pkg.registry} (hallucinated or unpublished)`
|
|
17625
17685
|
};
|
|
17626
17686
|
}
|
|
@@ -17653,7 +17713,7 @@ var PackageChecker = class {
|
|
|
17653
17713
|
packageName: pkg.name,
|
|
17654
17714
|
registry: pkg.registry,
|
|
17655
17715
|
verdict: "suspicious_age",
|
|
17656
|
-
confidence: 0.
|
|
17716
|
+
confidence: 0.6,
|
|
17657
17717
|
details: `Package '${pkg.name}' first published ${Math.floor(ageDays)} days ago (suspicious age)`,
|
|
17658
17718
|
ageDays
|
|
17659
17719
|
};
|
|
@@ -18044,7 +18104,7 @@ function extractFromRequirementsTxt(content) {
|
|
|
18044
18104
|
}
|
|
18045
18105
|
|
|
18046
18106
|
// ../core/dist/statusline.js
|
|
18047
|
-
var
|
|
18107
|
+
var import_node_path16 = require("node:path");
|
|
18048
18108
|
init_config();
|
|
18049
18109
|
init_file_utils();
|
|
18050
18110
|
var STATUS_PREFIX = "statusline-";
|
|
@@ -18053,15 +18113,17 @@ function sanitizeSessionId(sessionId) {
|
|
|
18053
18113
|
return sessionId.replace(/[^a-zA-Z0-9-]/g, "_");
|
|
18054
18114
|
}
|
|
18055
18115
|
function statusFilePath(sessionId) {
|
|
18056
|
-
return (0,
|
|
18116
|
+
return (0, import_node_path16.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
|
|
18057
18117
|
}
|
|
18058
18118
|
function emptyStatus() {
|
|
18119
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18059
18120
|
return {
|
|
18060
18121
|
denied: 0,
|
|
18061
18122
|
flagged: 0,
|
|
18062
18123
|
lastCategory: null,
|
|
18063
18124
|
lastReason: null,
|
|
18064
|
-
updatedAt:
|
|
18125
|
+
updatedAt: now,
|
|
18126
|
+
startedAt: now
|
|
18065
18127
|
};
|
|
18066
18128
|
}
|
|
18067
18129
|
async function readStatus(sessionId) {
|
|
@@ -18086,8 +18148,8 @@ async function updateSessionStatus(sessionId, verdict) {
|
|
|
18086
18148
|
}
|
|
18087
18149
|
|
|
18088
18150
|
// ../core/dist/threat-loader.js
|
|
18089
|
-
var
|
|
18090
|
-
var
|
|
18151
|
+
var import_promises7 = require("node:fs/promises");
|
|
18152
|
+
var import_node_path17 = require("node:path");
|
|
18091
18153
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
18092
18154
|
init_file_utils();
|
|
18093
18155
|
init_types2();
|
|
@@ -18096,7 +18158,6 @@ var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
|
|
|
18096
18158
|
"category",
|
|
18097
18159
|
"severity",
|
|
18098
18160
|
"confidence",
|
|
18099
|
-
"action",
|
|
18100
18161
|
"pattern",
|
|
18101
18162
|
"match_on",
|
|
18102
18163
|
"title"
|
|
@@ -18121,13 +18182,13 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18121
18182
|
const threats = [];
|
|
18122
18183
|
let files;
|
|
18123
18184
|
try {
|
|
18124
|
-
files = (await (0,
|
|
18185
|
+
files = (await (0, import_promises7.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
18125
18186
|
} catch {
|
|
18126
18187
|
logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
|
|
18127
18188
|
return threats;
|
|
18128
18189
|
}
|
|
18129
18190
|
for (const filename of files) {
|
|
18130
|
-
const filePath = (0,
|
|
18191
|
+
const filePath = (0, import_node_path17.join)(threatDir, filename);
|
|
18131
18192
|
let content;
|
|
18132
18193
|
try {
|
|
18133
18194
|
content = await getFileContent(filePath);
|
|
@@ -18164,8 +18225,8 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18164
18225
|
continue;
|
|
18165
18226
|
let compiledPattern;
|
|
18166
18227
|
try {
|
|
18167
|
-
const
|
|
18168
|
-
compiledPattern = new RegExp(record.pattern,
|
|
18228
|
+
const flags2 = record.case_insensitive === true ? "i" : "";
|
|
18229
|
+
compiledPattern = new RegExp(record.pattern, flags2);
|
|
18169
18230
|
} catch (e) {
|
|
18170
18231
|
logger.warn(`Skipping threat ${record.id}: invalid regex pattern`, {
|
|
18171
18232
|
error: String(e)
|
|
@@ -18174,19 +18235,28 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18174
18235
|
}
|
|
18175
18236
|
const rawMatchOn = record.match_on;
|
|
18176
18237
|
const matchOn = new Set(Array.isArray(rawMatchOn) ? rawMatchOn : [rawMatchOn]);
|
|
18238
|
+
const rawFlags = record.flags;
|
|
18239
|
+
const flags = Array.isArray(rawFlags) ? rawFlags : [];
|
|
18240
|
+
const confidence = Number(record.confidence);
|
|
18241
|
+
if (!Number.isFinite(confidence) || confidence <= 0 || confidence > 1) {
|
|
18242
|
+
logger.warn(`Skipping threat ${record.id}: invalid confidence value`, {
|
|
18243
|
+
confidence: record.confidence
|
|
18244
|
+
});
|
|
18245
|
+
continue;
|
|
18246
|
+
}
|
|
18177
18247
|
threats.push({
|
|
18178
18248
|
id: record.id,
|
|
18179
18249
|
version: typeof record.version === "number" ? record.version : void 0,
|
|
18180
18250
|
category: record.category,
|
|
18181
18251
|
severity: record.severity,
|
|
18182
|
-
confidence
|
|
18183
|
-
action: record.action,
|
|
18252
|
+
confidence,
|
|
18184
18253
|
pattern: record.pattern,
|
|
18185
18254
|
compiledPattern,
|
|
18186
18255
|
matchOn,
|
|
18187
18256
|
title: record.title,
|
|
18188
18257
|
expiresAt: parseExpiresAt(record.expires_at),
|
|
18189
|
-
revoked: false
|
|
18258
|
+
revoked: false,
|
|
18259
|
+
flags
|
|
18190
18260
|
});
|
|
18191
18261
|
}
|
|
18192
18262
|
}
|
|
@@ -18226,7 +18296,7 @@ function allowVerdict(source = "none") {
|
|
|
18226
18296
|
return {
|
|
18227
18297
|
decision: "allow",
|
|
18228
18298
|
category: "none",
|
|
18229
|
-
confidence:
|
|
18299
|
+
confidence: 0,
|
|
18230
18300
|
severity: "info",
|
|
18231
18301
|
source,
|
|
18232
18302
|
artifacts: [],
|
|
@@ -18234,6 +18304,11 @@ function allowVerdict(source = "none") {
|
|
|
18234
18304
|
reasons: []
|
|
18235
18305
|
};
|
|
18236
18306
|
}
|
|
18307
|
+
async function resolveEvaluationConfig(context, logger) {
|
|
18308
|
+
if (context.config)
|
|
18309
|
+
return context.config;
|
|
18310
|
+
return loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
|
|
18311
|
+
}
|
|
18237
18312
|
function shouldSkipPromptInjectionForLocalMarkdown(request) {
|
|
18238
18313
|
if (request.toolName !== "Write" && request.toolName !== "Edit")
|
|
18239
18314
|
return false;
|
|
@@ -18243,8 +18318,8 @@ function shouldSkipPromptInjectionForLocalMarkdown(request) {
|
|
|
18243
18318
|
}
|
|
18244
18319
|
async function evaluateToolCall(request, context) {
|
|
18245
18320
|
const logger = context.logger ?? nullLogger;
|
|
18246
|
-
const config2 = await
|
|
18247
|
-
const eventId = (0,
|
|
18321
|
+
const config2 = await resolveEvaluationConfig(context, logger);
|
|
18322
|
+
const eventId = (0, import_node_crypto6.randomUUID)();
|
|
18248
18323
|
logger.debug("Tool call evaluation started", {
|
|
18249
18324
|
eventId,
|
|
18250
18325
|
toolUseId: request.toolUseId,
|
|
@@ -18305,27 +18380,6 @@ async function evaluateToolCall(request, context) {
|
|
|
18305
18380
|
logEvaluationCompleted(verdict2);
|
|
18306
18381
|
return verdict2;
|
|
18307
18382
|
}
|
|
18308
|
-
try {
|
|
18309
|
-
const allowlist = await loadAllowlist(config2.allowlist, logger);
|
|
18310
|
-
if (isAllowlisted(allowlist, request.artifacts)) {
|
|
18311
|
-
const allowV = allowVerdict("allowlisted");
|
|
18312
|
-
await logVerdict(config2.logging, {
|
|
18313
|
-
sessionId: request.sessionId,
|
|
18314
|
-
toolName: request.toolName,
|
|
18315
|
-
toolInput: request.toolInput,
|
|
18316
|
-
verdict: allowV,
|
|
18317
|
-
userOverride: true,
|
|
18318
|
-
conversationId: request.conversationId,
|
|
18319
|
-
agentRuntime: request.agentRuntime,
|
|
18320
|
-
hookType: request.hookType,
|
|
18321
|
-
eventId,
|
|
18322
|
-
toolUseId: request.toolUseId
|
|
18323
|
-
});
|
|
18324
|
-
logEvaluationCompleted(allowV);
|
|
18325
|
-
return allowV;
|
|
18326
|
-
}
|
|
18327
|
-
} catch {
|
|
18328
|
-
}
|
|
18329
18383
|
const allowMatch = findAllowException(exceptions, request.artifacts);
|
|
18330
18384
|
if (allowMatch) {
|
|
18331
18385
|
const allowV = allowVerdict("exception");
|
|
@@ -18348,7 +18402,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18348
18402
|
return allowV;
|
|
18349
18403
|
}
|
|
18350
18404
|
} catch (error) {
|
|
18351
|
-
logger.debug("Exception
|
|
18405
|
+
logger.debug("Exception checks failed open", { error: String(error) });
|
|
18352
18406
|
}
|
|
18353
18407
|
let cache2 = null;
|
|
18354
18408
|
try {
|
|
@@ -18370,7 +18424,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18370
18424
|
if (shouldSkipPromptInjectionForLocalMarkdown(request)) {
|
|
18371
18425
|
threats = threats.filter((t) => t.category !== "prompt_injection");
|
|
18372
18426
|
}
|
|
18373
|
-
const trustedDomains = await loadTrustedDomains(context.
|
|
18427
|
+
const trustedDomains = await loadTrustedDomains(context.trustedDomainsDir, logger);
|
|
18374
18428
|
const heuristics = new HeuristicsEngine(threats, trustedDomains);
|
|
18375
18429
|
heuristicMatches = heuristics.match(request.artifacts);
|
|
18376
18430
|
}
|
|
@@ -18387,10 +18441,19 @@ async function evaluateToolCall(request, context) {
|
|
|
18387
18441
|
const amsiCheckResults = [];
|
|
18388
18442
|
if (config2.amsi_check.enabled && isAmsiSupported()) {
|
|
18389
18443
|
let amsiClient = null;
|
|
18444
|
+
let amsiLease = null;
|
|
18445
|
+
let ownsAmsiClient = false;
|
|
18390
18446
|
try {
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18447
|
+
if (context.acquireAmsiClientLease) {
|
|
18448
|
+
amsiLease = await context.acquireAmsiClientLease(logger);
|
|
18449
|
+
amsiClient = amsiLease?.client ?? null;
|
|
18450
|
+
} else {
|
|
18451
|
+
amsiClient = new AmsiClient(logger);
|
|
18452
|
+
ownsAmsiClient = true;
|
|
18453
|
+
await amsiClient.init();
|
|
18454
|
+
}
|
|
18455
|
+
if (amsiClient?.isAvailable) {
|
|
18456
|
+
const activeAmsiClient = amsiClient;
|
|
18394
18457
|
const scans = [];
|
|
18395
18458
|
if (request.toolName === "Bash") {
|
|
18396
18459
|
const command = request.toolInput.command ?? "";
|
|
@@ -18421,7 +18484,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18421
18484
|
}
|
|
18422
18485
|
}
|
|
18423
18486
|
for (const scan of scans) {
|
|
18424
|
-
const result = await
|
|
18487
|
+
const result = await activeAmsiClient.scanString(scan.scanType, scan.name, scan.content);
|
|
18425
18488
|
if (result) {
|
|
18426
18489
|
amsiCheckResults.push(result);
|
|
18427
18490
|
}
|
|
@@ -18429,16 +18492,26 @@ async function evaluateToolCall(request, context) {
|
|
|
18429
18492
|
}
|
|
18430
18493
|
} catch {
|
|
18431
18494
|
} finally {
|
|
18432
|
-
|
|
18495
|
+
try {
|
|
18496
|
+
if (amsiLease) {
|
|
18497
|
+
await amsiLease.release();
|
|
18498
|
+
} else if (ownsAmsiClient) {
|
|
18499
|
+
amsiClient?.close();
|
|
18500
|
+
}
|
|
18501
|
+
} catch {
|
|
18502
|
+
}
|
|
18433
18503
|
}
|
|
18434
18504
|
}
|
|
18505
|
+
const engine = new DecisionEngine(config2.sensitivity, logger);
|
|
18506
|
+
const preliminaryVerdict = await engine.decide({
|
|
18507
|
+
heuristicMatches,
|
|
18508
|
+
urlCheckResults,
|
|
18509
|
+
packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
|
|
18510
|
+
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0
|
|
18511
|
+
});
|
|
18435
18512
|
const allPiResults = [];
|
|
18436
|
-
const
|
|
18437
|
-
|
|
18438
|
-
const heuristicDeny = heuristicMatches.some((m) => m.threat.action === "block");
|
|
18439
|
-
const urlAlreadyDenied = cachedUrlVerdicts.size > 0 && [...cachedUrlVerdicts.values()].some((v) => v.verdict !== "allow");
|
|
18440
|
-
const urlCheckDenied = urlCheckResults.some((r) => r.isMalicious);
|
|
18441
|
-
if (config2.pi_check.enabled && !heuristicPiDeny && !heuristicDeny && !urlAlreadyDenied && !urlCheckDenied && request.toolName === "WebFetch") {
|
|
18513
|
+
const alreadyDenied = preliminaryVerdict.decision === "deny" || cachedUrlVerdicts.size > 0 && [...cachedUrlVerdicts.values()].some((v) => v.verdict === "deny");
|
|
18514
|
+
if (config2.pi_check.enabled && !alreadyDenied && request.toolName === "WebFetch") {
|
|
18442
18515
|
try {
|
|
18443
18516
|
const url = extractWebFetchUrl(request.toolInput);
|
|
18444
18517
|
if (url) {
|
|
@@ -18454,9 +18527,6 @@ async function evaluateToolCall(request, context) {
|
|
|
18454
18527
|
const result = await provider.checkContent(fetched.content, `WebFetch:${url}`);
|
|
18455
18528
|
if (result) {
|
|
18456
18529
|
allPiResults.push(result);
|
|
18457
|
-
if (result.risk >= config2.pi_check.high_risk_threshold) {
|
|
18458
|
-
piDenySignals.push(result);
|
|
18459
|
-
}
|
|
18460
18530
|
}
|
|
18461
18531
|
}
|
|
18462
18532
|
}
|
|
@@ -18465,34 +18535,41 @@ async function evaluateToolCall(request, context) {
|
|
|
18465
18535
|
} catch {
|
|
18466
18536
|
}
|
|
18467
18537
|
}
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18473
|
-
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
18477
|
-
|
|
18478
|
-
|
|
18479
|
-
|
|
18480
|
-
|
|
18538
|
+
let verdict;
|
|
18539
|
+
if (allPiResults.length > 0) {
|
|
18540
|
+
verdict = await engine.decide({
|
|
18541
|
+
heuristicMatches,
|
|
18542
|
+
urlCheckResults,
|
|
18543
|
+
packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
|
|
18544
|
+
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
|
|
18545
|
+
piCheckResults: allPiResults,
|
|
18546
|
+
piThresholds: {
|
|
18547
|
+
highRisk: DEFAULT_PI_HIGH_RISK_THRESHOLD,
|
|
18548
|
+
mediumRisk: DEFAULT_PI_MEDIUM_RISK_THRESHOLD
|
|
18549
|
+
}
|
|
18550
|
+
});
|
|
18551
|
+
} else {
|
|
18552
|
+
verdict = preliminaryVerdict;
|
|
18553
|
+
}
|
|
18554
|
+
if (cachedUrlVerdicts.size > 0 && verdict.decision !== "deny") {
|
|
18481
18555
|
for (const [url, cachedVerdict] of cachedUrlVerdicts) {
|
|
18482
18556
|
if (cachedVerdict.verdict === "allow") {
|
|
18483
18557
|
continue;
|
|
18484
18558
|
}
|
|
18485
|
-
verdict
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18492
|
-
|
|
18493
|
-
|
|
18494
|
-
|
|
18495
|
-
|
|
18559
|
+
if (cachedVerdict.verdict === "deny" || verdict.decision === "allow") {
|
|
18560
|
+
verdict = {
|
|
18561
|
+
decision: cachedVerdict.verdict,
|
|
18562
|
+
category: "network_egress",
|
|
18563
|
+
confidence: 1,
|
|
18564
|
+
severity: cachedVerdict.severity,
|
|
18565
|
+
source: `cache(${cachedVerdict.source})`,
|
|
18566
|
+
artifacts: [url],
|
|
18567
|
+
matchedThreatId: null,
|
|
18568
|
+
reasons: cachedVerdict.reasons
|
|
18569
|
+
};
|
|
18570
|
+
if (verdict.decision === "deny")
|
|
18571
|
+
break;
|
|
18572
|
+
}
|
|
18496
18573
|
}
|
|
18497
18574
|
}
|
|
18498
18575
|
await cacheUrlResults(urlCheckResults, cache2);
|
|
@@ -18559,7 +18636,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18559
18636
|
}
|
|
18560
18637
|
}
|
|
18561
18638
|
if (allPiResults.length > 0) {
|
|
18562
|
-
const piSnippetFloor =
|
|
18639
|
+
const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
|
|
18563
18640
|
auditSignals.pi_checks = allPiResults.map((r) => ({
|
|
18564
18641
|
risk: r.risk,
|
|
18565
18642
|
model_id: r.modelId,
|
|
@@ -18593,9 +18670,9 @@ async function evaluateToolCall(request, context) {
|
|
|
18593
18670
|
} catch (error) {
|
|
18594
18671
|
logger.debug("Audit verdict logging failed open", { error: String(error) });
|
|
18595
18672
|
}
|
|
18596
|
-
|
|
18673
|
+
const submitTelemetry = async (blocking) => {
|
|
18597
18674
|
try {
|
|
18598
|
-
await
|
|
18675
|
+
await sendCommunityIqTelemetry({
|
|
18599
18676
|
eventId,
|
|
18600
18677
|
agentRuntime: request.agentRuntime,
|
|
18601
18678
|
agentRuntimeVersion: request.agentRuntimeVersion,
|
|
@@ -18604,11 +18681,18 @@ async function evaluateToolCall(request, context) {
|
|
|
18604
18681
|
content: resolvedContent,
|
|
18605
18682
|
signals: resolvedSignals,
|
|
18606
18683
|
communityIqEnabled: config2.community_iq,
|
|
18684
|
+
config: config2,
|
|
18685
|
+
blocking,
|
|
18607
18686
|
logger
|
|
18608
18687
|
});
|
|
18609
18688
|
} catch (error) {
|
|
18610
|
-
logger.debug("
|
|
18689
|
+
logger.debug("Community IQ telemetry failed open", { error: String(error) });
|
|
18611
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);
|
|
18612
18696
|
}
|
|
18613
18697
|
if (verdict.decision !== "allow") {
|
|
18614
18698
|
try {
|
|
@@ -18617,7 +18701,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18617
18701
|
logger.debug("Session status update failed open", { error: String(error) });
|
|
18618
18702
|
}
|
|
18619
18703
|
}
|
|
18620
|
-
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);
|
|
18621
18705
|
if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
|
|
18622
18706
|
verdict.piWarnings = piWarnings;
|
|
18623
18707
|
logEvaluationCompleted(verdict);
|
|
@@ -18690,21 +18774,41 @@ async function checkPackages(request, config2, cache2, logger) {
|
|
|
18690
18774
|
}
|
|
18691
18775
|
if (!parsedPackages || parsedPackages.length === 0)
|
|
18692
18776
|
return results;
|
|
18777
|
+
const DENY_CLASS_PKG_VERDICTS = /* @__PURE__ */ new Set(["malicious", "not_found"]);
|
|
18778
|
+
const ASK_CLASS_PKG_VERDICTS = /* @__PURE__ */ new Set(["suspicious_age", "unknown"]);
|
|
18779
|
+
const VALID_PKG_VERDICTS = /* @__PURE__ */ new Set([...DENY_CLASS_PKG_VERDICTS, ...ASK_CLASS_PKG_VERDICTS]);
|
|
18693
18780
|
const uncached = [];
|
|
18694
18781
|
for (const pkg of parsedPackages) {
|
|
18695
18782
|
const cacheKey2 = `${pkg.registry}:${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`;
|
|
18696
18783
|
const cached = cache2?.getPackage(cacheKey2);
|
|
18697
|
-
if (cached
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18784
|
+
if (!cached) {
|
|
18785
|
+
uncached.push(pkg);
|
|
18786
|
+
continue;
|
|
18787
|
+
}
|
|
18788
|
+
if (cached.verdict === "allow")
|
|
18789
|
+
continue;
|
|
18790
|
+
const rawVerdict = cached.packageVerdict;
|
|
18791
|
+
const rawConf = cached.packageConfidence;
|
|
18792
|
+
const verdictValid = rawVerdict !== void 0 && VALID_PKG_VERDICTS.has(rawVerdict);
|
|
18793
|
+
const confValid = rawConf !== void 0 && Number.isFinite(rawConf) && rawConf > 0 && rawConf <= 1;
|
|
18794
|
+
const consistent = rawVerdict === void 0 || !verdictValid || cached.verdict === "deny" && DENY_CLASS_PKG_VERDICTS.has(rawVerdict) || cached.verdict === "ask" && ASK_CLASS_PKG_VERDICTS.has(rawVerdict);
|
|
18795
|
+
if (!verdictValid || !confValid || !consistent) {
|
|
18796
|
+
logger.warn("Package cache entry has invalid metadata; re-querying live", {
|
|
18797
|
+
cacheKey: cacheKey2,
|
|
18798
|
+
packageVerdict: rawVerdict,
|
|
18799
|
+
packageConfidence: rawConf,
|
|
18800
|
+
cachedVerdict: cached.verdict
|
|
18704
18801
|
});
|
|
18705
|
-
} else if (!cached) {
|
|
18706
18802
|
uncached.push(pkg);
|
|
18803
|
+
continue;
|
|
18707
18804
|
}
|
|
18805
|
+
results.push({
|
|
18806
|
+
packageName: pkg.name,
|
|
18807
|
+
registry: pkg.registry,
|
|
18808
|
+
verdict: rawVerdict,
|
|
18809
|
+
confidence: rawConf,
|
|
18810
|
+
details: cached.reasons.join("; ")
|
|
18811
|
+
});
|
|
18708
18812
|
}
|
|
18709
18813
|
if (uncached.length > 0) {
|
|
18710
18814
|
const checker = new PackageChecker({
|
|
@@ -18724,7 +18828,9 @@ async function checkPackages(request, config2, cache2, logger) {
|
|
|
18724
18828
|
verdict: result.verdict === "clean" ? "allow" : isCritical ? "deny" : "ask",
|
|
18725
18829
|
severity: isCritical ? "critical" : "warning",
|
|
18726
18830
|
reasons: [result.details],
|
|
18727
|
-
source: "package_check"
|
|
18831
|
+
source: "package_check",
|
|
18832
|
+
packageVerdict: result.verdict,
|
|
18833
|
+
packageConfidence: result.confidence
|
|
18728
18834
|
}, result.ageDays ?? null);
|
|
18729
18835
|
}
|
|
18730
18836
|
}
|
|
@@ -18737,7 +18843,7 @@ function createPiProvider(config2, _context, logger) {
|
|
|
18737
18843
|
return new BundledPiProvider({
|
|
18738
18844
|
modelPath: config2.pi_check.model_path,
|
|
18739
18845
|
maxContentLength: config2.pi_check.max_content_length,
|
|
18740
|
-
mediumRiskThreshold:
|
|
18846
|
+
mediumRiskThreshold: DEFAULT_PI_MEDIUM_RISK_THRESHOLD,
|
|
18741
18847
|
logger
|
|
18742
18848
|
});
|
|
18743
18849
|
}
|
|
@@ -18750,9 +18856,9 @@ var PAD = 12;
|
|
|
18750
18856
|
var SEPARATOR_WIDTH = 48;
|
|
18751
18857
|
function severityEmoji(severity) {
|
|
18752
18858
|
const s = severity.toLowerCase();
|
|
18753
|
-
if (s === "critical"
|
|
18859
|
+
if (s === "critical")
|
|
18754
18860
|
return "\u{1F6A8}";
|
|
18755
|
-
if (s === "
|
|
18861
|
+
if (s === "warning")
|
|
18756
18862
|
return "\u26A0\uFE0F";
|
|
18757
18863
|
return "\u2139\uFE0F";
|
|
18758
18864
|
}
|
|
@@ -18773,20 +18879,20 @@ ${formatUpdateNotice(versionCheck)}`;
|
|
|
18773
18879
|
}
|
|
18774
18880
|
return base;
|
|
18775
18881
|
}
|
|
18776
|
-
function
|
|
18882
|
+
function totalNonInfo(results) {
|
|
18777
18883
|
let n = 0;
|
|
18778
18884
|
for (const r of results) {
|
|
18779
18885
|
for (const f of r.findings) {
|
|
18780
|
-
if (f.severity
|
|
18886
|
+
if (f.severity !== "info")
|
|
18781
18887
|
n++;
|
|
18782
18888
|
}
|
|
18783
18889
|
}
|
|
18784
18890
|
return n;
|
|
18785
18891
|
}
|
|
18786
|
-
function
|
|
18892
|
+
function findFirstNonInfo(results) {
|
|
18787
18893
|
for (const r of results) {
|
|
18788
18894
|
for (const f of r.findings) {
|
|
18789
|
-
if (f.severity
|
|
18895
|
+
if (f.severity !== "info") {
|
|
18790
18896
|
return { plugin: r.plugin, finding: f };
|
|
18791
18897
|
}
|
|
18792
18898
|
}
|
|
@@ -18799,10 +18905,10 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
|
|
|
18799
18905
|
const MAX_FINDINGS = 5;
|
|
18800
18906
|
let first = true;
|
|
18801
18907
|
for (const result of results) {
|
|
18802
|
-
const
|
|
18803
|
-
if (
|
|
18908
|
+
const nonInfo = result.findings.filter((f) => f.severity !== "info");
|
|
18909
|
+
if (nonInfo.length === 0)
|
|
18804
18910
|
continue;
|
|
18805
|
-
for (const f of
|
|
18911
|
+
for (const f of nonInfo.slice(0, MAX_FINDINGS)) {
|
|
18806
18912
|
if (!first)
|
|
18807
18913
|
lines.push("");
|
|
18808
18914
|
first = false;
|
|
@@ -18814,8 +18920,6 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
|
|
|
18814
18920
|
lines.push(kv("Artifact", f.artifact));
|
|
18815
18921
|
}
|
|
18816
18922
|
lines.push(kv("Source", f.sourceFile));
|
|
18817
|
-
const actionLabel = f.action === "block" ? "Blocked" : "Flagged";
|
|
18818
|
-
lines.push(kv("Action", actionLabel));
|
|
18819
18923
|
if (f.recommendations && f.recommendations.length > 0) {
|
|
18820
18924
|
lines.push(kv("Recommendations", ""));
|
|
18821
18925
|
for (const rec of f.recommendations) {
|
|
@@ -18823,7 +18927,7 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
|
|
|
18823
18927
|
}
|
|
18824
18928
|
}
|
|
18825
18929
|
}
|
|
18826
|
-
const overflow =
|
|
18930
|
+
const overflow = nonInfo.length - MAX_FINDINGS;
|
|
18827
18931
|
if (overflow > 0) {
|
|
18828
18932
|
lines.push("");
|
|
18829
18933
|
lines.push(` ... and ${overflow} more findings`);
|
|
@@ -18836,8 +18940,8 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
|
|
|
18836
18940
|
return lines.join("\n");
|
|
18837
18941
|
}
|
|
18838
18942
|
function formatThreatBannerCompact(version, results, versionCheck, branding2) {
|
|
18839
|
-
const total =
|
|
18840
|
-
const first =
|
|
18943
|
+
const total = totalNonInfo(results);
|
|
18944
|
+
const first = findFirstNonInfo(results);
|
|
18841
18945
|
if (!first || total === 0) {
|
|
18842
18946
|
return formatStartupClean(version, versionCheck, branding2);
|
|
18843
18947
|
}
|
|
@@ -18902,20 +19006,230 @@ function truncateValue(value) {
|
|
|
18902
19006
|
function summarizeArtifacts(artifacts) {
|
|
18903
19007
|
if (artifacts.length === 0)
|
|
18904
19008
|
return "none";
|
|
18905
|
-
|
|
19009
|
+
const maxArtifacts = 3;
|
|
19010
|
+
const summary = artifacts.slice(0, maxArtifacts).map((artifact) => `${artifact.type} '${truncateValue(artifact.value)}'`).join(", ");
|
|
19011
|
+
const overflow = artifacts.length - maxArtifacts;
|
|
19012
|
+
return overflow > 0 ? `${summary}, ... and ${overflow} more` : summary;
|
|
18906
19013
|
}
|
|
18907
19014
|
function formatDenyMessage(verdict, branding2 = defaultBranding) {
|
|
18908
|
-
const
|
|
19015
|
+
const maxReasons = 5;
|
|
19016
|
+
const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, maxReasons).join("; ") + (verdict.reasons.length > maxReasons ? `; ... and ${verdict.reasons.length - maxReasons} more` : "") : verdict.category;
|
|
18909
19017
|
return [
|
|
18910
19018
|
`${branding2.name} blocked this action.`,
|
|
18911
19019
|
`Severity: ${verdict.severity}`,
|
|
18912
19020
|
`Category: ${verdict.category}`,
|
|
18913
|
-
`Reason: ${reasons}
|
|
19021
|
+
`Reason: ${reasons}`,
|
|
19022
|
+
`If this is a false positive, use the sage_report_false_positive MCP tool to report it.`
|
|
18914
19023
|
].join("\n");
|
|
18915
19024
|
}
|
|
18916
19025
|
|
|
18917
|
-
// ../core/dist/
|
|
19026
|
+
// ../core/dist/install-state.js
|
|
19027
|
+
var import_node_path18 = require("node:path");
|
|
19028
|
+
init_config();
|
|
19029
|
+
init_file_utils();
|
|
19030
|
+
|
|
19031
|
+
// ../core/dist/notices.js
|
|
18918
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
|
|
19044
|
+
};
|
|
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
|
+
}
|
|
18919
19233
|
|
|
18920
19234
|
// ../core/dist/model-download.js
|
|
18921
19235
|
init_types2();
|
|
@@ -18988,8 +19302,8 @@ function createOperationalLogger(config2, runtime) {
|
|
|
18988
19302
|
try {
|
|
18989
19303
|
const result = await Promise.race([
|
|
18990
19304
|
Promise.allSettled([...pendingWrites]).then(() => "drained"),
|
|
18991
|
-
new Promise((
|
|
18992
|
-
timeout = setTimeout(() =>
|
|
19305
|
+
new Promise((resolve10) => {
|
|
19306
|
+
timeout = setTimeout(() => resolve10("timeout"), remainingMs);
|
|
18993
19307
|
timeout.unref?.();
|
|
18994
19308
|
})
|
|
18995
19309
|
]);
|
|
@@ -19037,30 +19351,32 @@ function createOperationalLogger(config2, runtime) {
|
|
|
19037
19351
|
}
|
|
19038
19352
|
|
|
19039
19353
|
// ../core/dist/plugin-scan-cache.js
|
|
19040
|
-
var
|
|
19041
|
-
var
|
|
19042
|
-
var
|
|
19354
|
+
var import_node_crypto7 = require("node:crypto");
|
|
19355
|
+
var import_promises8 = require("node:fs/promises");
|
|
19356
|
+
var import_node_path19 = require("node:path");
|
|
19357
|
+
init_config();
|
|
19043
19358
|
init_file_utils();
|
|
19044
19359
|
init_types2();
|
|
19045
|
-
var DEFAULT_CACHE_PATH = (0,
|
|
19046
|
-
var
|
|
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;
|
|
19047
19363
|
function cacheKey(pluginKey, version, lastUpdated) {
|
|
19048
19364
|
return `${pluginKey}:${version}:${lastUpdated}`;
|
|
19049
19365
|
}
|
|
19050
19366
|
async function computeConfigHash(sageVersion, ...dirs) {
|
|
19051
|
-
const h = (0,
|
|
19367
|
+
const h = (0, import_node_crypto7.createHash)("sha256");
|
|
19052
19368
|
if (sageVersion)
|
|
19053
19369
|
h.update(sageVersion);
|
|
19054
19370
|
for (const dir of dirs) {
|
|
19055
19371
|
let files;
|
|
19056
19372
|
try {
|
|
19057
|
-
files = (await (0,
|
|
19373
|
+
files = (await (0, import_promises8.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
19058
19374
|
} catch {
|
|
19059
19375
|
continue;
|
|
19060
19376
|
}
|
|
19061
19377
|
for (const file of files) {
|
|
19062
19378
|
try {
|
|
19063
|
-
const content = await getFileContent((0,
|
|
19379
|
+
const content = await getFileContent((0, import_node_path19.join)(dir, file));
|
|
19064
19380
|
h.update(content);
|
|
19065
19381
|
} catch {
|
|
19066
19382
|
}
|
|
@@ -19077,6 +19393,14 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
|
|
|
19077
19393
|
}
|
|
19078
19394
|
try {
|
|
19079
19395
|
const data = JSON.parse(raw);
|
|
19396
|
+
const storedVersion = data.schema_version ?? 1;
|
|
19397
|
+
if (storedVersion !== SCHEMA_VERSION) {
|
|
19398
|
+
logger.debug("Schema version changed, invalidating plugin scan cache", {
|
|
19399
|
+
from: storedVersion,
|
|
19400
|
+
to: SCHEMA_VERSION
|
|
19401
|
+
});
|
|
19402
|
+
return { configHash, entries: {} };
|
|
19403
|
+
}
|
|
19080
19404
|
const storedHash = data.config_hash ?? "";
|
|
19081
19405
|
if (configHash && storedHash !== configHash) {
|
|
19082
19406
|
logger.debug("Config hash changed, invalidating plugin scan cache", {
|
|
@@ -19104,6 +19428,7 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
|
|
|
19104
19428
|
async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nullLogger) {
|
|
19105
19429
|
try {
|
|
19106
19430
|
const data = {
|
|
19431
|
+
schema_version: SCHEMA_VERSION,
|
|
19107
19432
|
config_hash: cache2.configHash,
|
|
19108
19433
|
entries: Object.fromEntries(Object.entries(cache2.entries).map(([key, entry]) => [
|
|
19109
19434
|
key,
|
|
@@ -19120,7 +19445,7 @@ async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nu
|
|
|
19120
19445
|
logger.warn(`Failed to save scan cache to ${cachePath}`, { error: String(e) });
|
|
19121
19446
|
}
|
|
19122
19447
|
}
|
|
19123
|
-
function isCached(cache2, pluginKey, version, lastUpdated) {
|
|
19448
|
+
function isCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
|
|
19124
19449
|
const key = cacheKey(pluginKey, version, lastUpdated);
|
|
19125
19450
|
const entry = cache2.entries[key];
|
|
19126
19451
|
if (!entry)
|
|
@@ -19128,13 +19453,13 @@ function isCached(cache2, pluginKey, version, lastUpdated) {
|
|
|
19128
19453
|
try {
|
|
19129
19454
|
const scannedAt = new Date(entry.scannedAt);
|
|
19130
19455
|
const age = Date.now() - scannedAt.getTime();
|
|
19131
|
-
return age <
|
|
19456
|
+
return age < ttlMs;
|
|
19132
19457
|
} catch {
|
|
19133
19458
|
return false;
|
|
19134
19459
|
}
|
|
19135
19460
|
}
|
|
19136
|
-
function getCached(cache2, pluginKey, version, lastUpdated) {
|
|
19137
|
-
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))
|
|
19138
19463
|
return null;
|
|
19139
19464
|
const key = cacheKey(pluginKey, version, lastUpdated);
|
|
19140
19465
|
return cache2.entries[key] ?? null;
|
|
@@ -19150,18 +19475,22 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
|
|
|
19150
19475
|
}
|
|
19151
19476
|
|
|
19152
19477
|
// ../core/dist/plugin-scanner.js
|
|
19153
|
-
var
|
|
19154
|
-
var
|
|
19155
|
-
var
|
|
19478
|
+
var import_node_crypto9 = require("node:crypto");
|
|
19479
|
+
var import_promises11 = require("node:fs/promises");
|
|
19480
|
+
var import_node_path23 = require("node:path");
|
|
19481
|
+
init_config();
|
|
19156
19482
|
init_file_utils();
|
|
19157
19483
|
|
|
19158
19484
|
// ../core/dist/skill-id.js
|
|
19159
|
-
var
|
|
19160
|
-
var
|
|
19161
|
-
var
|
|
19485
|
+
var import_node_crypto8 = require("node:crypto");
|
|
19486
|
+
var import_promises9 = require("node:fs/promises");
|
|
19487
|
+
var import_node_path20 = require("node:path");
|
|
19162
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
|
+
};
|
|
19163
19492
|
function isContained(childReal, rootReal) {
|
|
19164
|
-
return childReal === rootReal || childReal.startsWith(`${rootReal}${
|
|
19493
|
+
return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path20.sep}`);
|
|
19165
19494
|
}
|
|
19166
19495
|
function normalizePath(p) {
|
|
19167
19496
|
let out = p.replace(/\\/g, "/").normalize("NFC");
|
|
@@ -19171,47 +19500,51 @@ function normalizePath(p) {
|
|
|
19171
19500
|
}
|
|
19172
19501
|
return out;
|
|
19173
19502
|
}
|
|
19174
|
-
async function entriesFromDirectory(dirPath) {
|
|
19175
|
-
const absDir = (0,
|
|
19176
|
-
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);
|
|
19177
19506
|
const entries = [];
|
|
19178
19507
|
const visited = /* @__PURE__ */ new Set();
|
|
19508
|
+
let total = 0;
|
|
19179
19509
|
async function walk(currentPath) {
|
|
19180
|
-
const real = await (0,
|
|
19510
|
+
const real = await (0, import_promises9.realpath)(currentPath);
|
|
19181
19511
|
if (visited.has(real))
|
|
19182
19512
|
return;
|
|
19183
19513
|
if (!isContained(real, rootReal))
|
|
19184
19514
|
return;
|
|
19185
19515
|
visited.add(real);
|
|
19186
|
-
const items = await (0,
|
|
19516
|
+
const items = await (0, import_promises9.readdir)(currentPath);
|
|
19187
19517
|
const dirs = [];
|
|
19188
19518
|
const files = [];
|
|
19189
19519
|
for (const item of items) {
|
|
19190
|
-
const fullPath = (0,
|
|
19191
|
-
const st = await (0,
|
|
19520
|
+
const fullPath = (0, import_node_path20.join)(currentPath, item);
|
|
19521
|
+
const st = await (0, import_promises9.stat)(fullPath);
|
|
19192
19522
|
if (st.isDirectory())
|
|
19193
19523
|
dirs.push(item);
|
|
19194
19524
|
else if (st.isFile())
|
|
19195
|
-
files.push(item);
|
|
19525
|
+
files.push({ name: item, size: st.size });
|
|
19196
19526
|
}
|
|
19197
19527
|
dirs.sort();
|
|
19198
|
-
files.sort();
|
|
19528
|
+
files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
19199
19529
|
for (const d of dirs) {
|
|
19200
|
-
const fullPath = (0,
|
|
19201
|
-
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, "/");
|
|
19202
19532
|
entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
|
|
19203
19533
|
await walk(fullPath);
|
|
19204
19534
|
}
|
|
19205
19535
|
for (const f of files) {
|
|
19206
|
-
const fullPath = (0,
|
|
19207
|
-
const fileReal = await (0,
|
|
19536
|
+
const fullPath = (0, import_node_path20.join)(currentPath, f.name);
|
|
19537
|
+
const fileReal = await (0, import_promises9.realpath)(fullPath);
|
|
19208
19538
|
if (!isContained(fileReal, rootReal))
|
|
19209
19539
|
continue;
|
|
19210
|
-
|
|
19540
|
+
total += f.size;
|
|
19541
|
+
if (total > maxBytes)
|
|
19542
|
+
throw new SkillTooLargeError();
|
|
19543
|
+
const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19211
19544
|
entries.push({
|
|
19212
19545
|
entryPath: relPath,
|
|
19213
19546
|
isDir: false,
|
|
19214
|
-
content: await (0,
|
|
19547
|
+
content: await (0, import_promises9.readFile)(fullPath)
|
|
19215
19548
|
});
|
|
19216
19549
|
}
|
|
19217
19550
|
}
|
|
@@ -19230,98 +19563,363 @@ function computeSkillId(entries) {
|
|
|
19230
19563
|
})).filter((e) => e.entryPath !== "");
|
|
19231
19564
|
}
|
|
19232
19565
|
}
|
|
19233
|
-
normalized = normalized.filter((e) => e.entryPath !== "skill.sig");
|
|
19234
|
-
const fileHashes = {};
|
|
19235
|
-
const treeEntries = [];
|
|
19236
|
-
for (const e of normalized) {
|
|
19237
|
-
const contentHash = (0,
|
|
19238
|
-
const entryType = e.isDir ? "dir" : "file";
|
|
19239
|
-
treeEntries.push({ type: entryType, entryPath: e.entryPath, hash: contentHash });
|
|
19240
|
-
if (!e.isDir)
|
|
19241
|
-
fileHashes[e.entryPath] = contentHash;
|
|
19566
|
+
normalized = normalized.filter((e) => e.entryPath !== "skill.sig");
|
|
19567
|
+
const fileHashes = {};
|
|
19568
|
+
const treeEntries = [];
|
|
19569
|
+
for (const e of normalized) {
|
|
19570
|
+
const contentHash = (0, import_node_crypto8.createHash)("sha256").update(e.isDir ? Buffer.alloc(0) : e.content).digest("hex");
|
|
19571
|
+
const entryType = e.isDir ? "dir" : "file";
|
|
19572
|
+
treeEntries.push({ type: entryType, entryPath: e.entryPath, hash: contentHash });
|
|
19573
|
+
if (!e.isDir)
|
|
19574
|
+
fileHashes[e.entryPath] = contentHash;
|
|
19575
|
+
}
|
|
19576
|
+
treeEntries.sort((a, b) => a.entryPath < b.entryPath ? -1 : a.entryPath > b.entryPath ? 1 : 0);
|
|
19577
|
+
const skillHasher = (0, import_node_crypto8.createHash)("sha256");
|
|
19578
|
+
for (const e of treeEntries) {
|
|
19579
|
+
skillHasher.update(Buffer.from(`${e.type}\0${e.entryPath}\0${e.hash}
|
|
19580
|
+
`, "utf8"));
|
|
19581
|
+
}
|
|
19582
|
+
return { skillId: skillHasher.digest("hex"), fileHashes };
|
|
19583
|
+
}
|
|
19584
|
+
async function findSkillPackagesWithMtime(rootDir) {
|
|
19585
|
+
const found = [];
|
|
19586
|
+
let rootReal;
|
|
19587
|
+
try {
|
|
19588
|
+
rootReal = await (0, import_promises9.realpath)((0, import_node_path20.resolve)(rootDir));
|
|
19589
|
+
} catch {
|
|
19590
|
+
return found;
|
|
19591
|
+
}
|
|
19592
|
+
const visited = /* @__PURE__ */ new Set();
|
|
19593
|
+
async function walk(currentPath) {
|
|
19594
|
+
let real;
|
|
19595
|
+
try {
|
|
19596
|
+
real = await (0, import_promises9.realpath)(currentPath);
|
|
19597
|
+
} catch {
|
|
19598
|
+
return 0;
|
|
19599
|
+
}
|
|
19600
|
+
if (visited.has(real))
|
|
19601
|
+
return 0;
|
|
19602
|
+
if (!isContained(real, rootReal))
|
|
19603
|
+
return 0;
|
|
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;
|
|
19614
|
+
let items;
|
|
19615
|
+
try {
|
|
19616
|
+
items = await (0, import_promises9.readdir)(currentPath);
|
|
19617
|
+
} catch {
|
|
19618
|
+
return subtreeMax;
|
|
19619
|
+
}
|
|
19620
|
+
items.sort();
|
|
19621
|
+
let entry;
|
|
19622
|
+
if (items.includes("SKILL.md")) {
|
|
19623
|
+
try {
|
|
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
|
+
}
|
|
19628
|
+
} catch {
|
|
19629
|
+
}
|
|
19630
|
+
}
|
|
19631
|
+
for (const item of items) {
|
|
19632
|
+
if (SKIP_DIRS.has(item))
|
|
19633
|
+
continue;
|
|
19634
|
+
const childMax = await walk((0, import_node_path20.join)(currentPath, item));
|
|
19635
|
+
if (childMax > subtreeMax)
|
|
19636
|
+
subtreeMax = childMax;
|
|
19637
|
+
}
|
|
19638
|
+
if (entry)
|
|
19639
|
+
entry.newestMtimeMs = subtreeMax;
|
|
19640
|
+
return subtreeMax;
|
|
19641
|
+
}
|
|
19642
|
+
await walk((0, import_node_path20.resolve)(rootDir));
|
|
19643
|
+
return found;
|
|
19644
|
+
}
|
|
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);
|
|
19687
|
+
const out = [];
|
|
19688
|
+
for (const folder of folders) {
|
|
19689
|
+
try {
|
|
19690
|
+
const entries = await entriesFromDirectory(folder, MAX_SKILL_BYTES);
|
|
19691
|
+
const { skillId } = computeSkillId(entries);
|
|
19692
|
+
out.push({ folder, skillId });
|
|
19693
|
+
} catch (e) {
|
|
19694
|
+
if (e instanceof SkillTooLargeError) {
|
|
19695
|
+
logger?.warn("Skill directory exceeds 50 MB limit; skipping", { folder });
|
|
19696
|
+
}
|
|
19697
|
+
}
|
|
19698
|
+
}
|
|
19699
|
+
return out;
|
|
19700
|
+
}
|
|
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);
|
|
19242
19821
|
}
|
|
19243
|
-
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
|
|
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);
|
|
19248
19833
|
}
|
|
19249
|
-
return
|
|
19834
|
+
return merged.length > 0 ? merged : void 0;
|
|
19250
19835
|
}
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
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;
|
|
19254
19841
|
try {
|
|
19255
|
-
|
|
19256
|
-
if (!rootStat.isDirectory())
|
|
19257
|
-
return found;
|
|
19258
|
-
rootReal = await (0, import_promises8.realpath)((0, import_node_path17.resolve)(rootDir));
|
|
19842
|
+
raw = await getFileContent(cachePath);
|
|
19259
19843
|
} catch {
|
|
19260
|
-
return
|
|
19844
|
+
return { entries: {} };
|
|
19261
19845
|
}
|
|
19262
|
-
|
|
19263
|
-
|
|
19264
|
-
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
return;
|
|
19272
|
-
if (!isContained(real, rootReal))
|
|
19273
|
-
return;
|
|
19274
|
-
visited.add(real);
|
|
19275
|
-
let items;
|
|
19276
|
-
try {
|
|
19277
|
-
items = await (0, import_promises8.readdir)(dir);
|
|
19278
|
-
} catch {
|
|
19279
|
-
return;
|
|
19280
|
-
}
|
|
19281
|
-
items.sort();
|
|
19282
|
-
if (items.includes("SKILL.md")) {
|
|
19283
|
-
try {
|
|
19284
|
-
const st = await (0, import_promises8.stat)((0, import_node_path17.join)(dir, "SKILL.md"));
|
|
19285
|
-
if (st.isFile())
|
|
19286
|
-
found.push(dir);
|
|
19287
|
-
} catch {
|
|
19288
|
-
}
|
|
19289
|
-
}
|
|
19290
|
-
for (const item of items) {
|
|
19291
|
-
if (SKIP_DIRS.has(item))
|
|
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)
|
|
19292
19855
|
continue;
|
|
19293
|
-
const
|
|
19294
|
-
|
|
19295
|
-
|
|
19296
|
-
st = await (0, import_promises8.stat)(fullPath);
|
|
19297
|
-
} catch {
|
|
19856
|
+
const ts = Date.parse(analyzedAt);
|
|
19857
|
+
if (!isFreshTimestamp(ts, ttlMs, now)) {
|
|
19858
|
+
pruned++;
|
|
19298
19859
|
continue;
|
|
19299
19860
|
}
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
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
|
+
};
|
|
19303
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: {} };
|
|
19304
19878
|
}
|
|
19305
|
-
await walk((0, import_node_path17.resolve)(rootDir));
|
|
19306
|
-
return found;
|
|
19307
19879
|
}
|
|
19308
|
-
async function
|
|
19309
|
-
|
|
19310
|
-
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
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) });
|
|
19318
19901
|
}
|
|
19319
|
-
|
|
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
|
+
};
|
|
19320
19919
|
}
|
|
19321
19920
|
|
|
19322
19921
|
// ../core/dist/plugin-scanner.js
|
|
19323
19922
|
init_types2();
|
|
19324
|
-
var DEFAULT_PLUGINS_REGISTRY = (0, import_node_path18.join)(getHomeDir(), ".claude", "plugins", "installed_plugins.json");
|
|
19325
19923
|
var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
19326
19924
|
".py",
|
|
19327
19925
|
".js",
|
|
@@ -19344,15 +19942,34 @@ var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
|
19344
19942
|
var MAX_FILE_SIZE = 512 * 1024;
|
|
19345
19943
|
async function walkPluginFiles(installPath, logger) {
|
|
19346
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
|
+
}
|
|
19347
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);
|
|
19348
19965
|
let stats;
|
|
19349
19966
|
try {
|
|
19350
|
-
stats = await (0,
|
|
19967
|
+
stats = await (0, import_promises11.stat)(dirOrFile);
|
|
19351
19968
|
} catch {
|
|
19352
19969
|
return;
|
|
19353
19970
|
}
|
|
19354
19971
|
if (stats.isFile()) {
|
|
19355
|
-
if (SCANNABLE_EXTENSIONS2.has((0,
|
|
19972
|
+
if (SCANNABLE_EXTENSIONS2.has((0, import_node_path23.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
|
|
19356
19973
|
files.push(dirOrFile);
|
|
19357
19974
|
}
|
|
19358
19975
|
return;
|
|
@@ -19360,32 +19977,41 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
19360
19977
|
if (stats.isDirectory()) {
|
|
19361
19978
|
let entries;
|
|
19362
19979
|
try {
|
|
19363
|
-
entries = await (0,
|
|
19980
|
+
entries = await (0, import_promises11.readdir)(dirOrFile);
|
|
19364
19981
|
} catch {
|
|
19365
19982
|
return;
|
|
19366
19983
|
}
|
|
19367
19984
|
for (const entry of entries) {
|
|
19368
19985
|
if (SKIP_DIRS.has(entry))
|
|
19369
19986
|
continue;
|
|
19370
|
-
const fullPath = (0,
|
|
19987
|
+
const fullPath = (0, import_node_path23.join)(dirOrFile, entry);
|
|
19371
19988
|
await walk(fullPath);
|
|
19372
19989
|
}
|
|
19373
19990
|
}
|
|
19374
19991
|
}
|
|
19375
19992
|
try {
|
|
19376
|
-
await walk(installPath);
|
|
19993
|
+
await walk((0, import_node_path23.resolve)(installPath));
|
|
19377
19994
|
} catch (e) {
|
|
19378
19995
|
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
19379
19996
|
}
|
|
19380
19997
|
return files;
|
|
19381
19998
|
}
|
|
19382
19999
|
async function scanPlugin(plugin, options = {}) {
|
|
19383
|
-
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;
|
|
19384
20001
|
const result = { plugin, findings: [] };
|
|
19385
|
-
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);
|
|
19386
20011
|
const files = await walkPluginFiles(plugin.installPath, logger);
|
|
19387
20012
|
if (files.length === 0) {
|
|
19388
|
-
await skillCheckPromise
|
|
20013
|
+
if (await skillCheckPromise)
|
|
20014
|
+
result.deferCache = true;
|
|
19389
20015
|
return result;
|
|
19390
20016
|
}
|
|
19391
20017
|
const allUrls = [];
|
|
@@ -19401,17 +20027,15 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19401
20027
|
}
|
|
19402
20028
|
if (amsiClient) {
|
|
19403
20029
|
try {
|
|
19404
|
-
const scanName = `${plugin.key}/${(0,
|
|
20030
|
+
const scanName = `${plugin.key}/${(0, import_node_path23.relative)(plugin.installPath, filePath)}`;
|
|
19405
20031
|
const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
|
|
19406
20032
|
if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
|
|
19407
20033
|
result.findings.push({
|
|
19408
20034
|
threatId: "AMSI_SCAN",
|
|
19409
20035
|
title: `AMSI detection (result=${amsiResult.amsiResult})`,
|
|
19410
20036
|
severity: "critical",
|
|
19411
|
-
confidence: 1,
|
|
19412
|
-
action: "block",
|
|
19413
20037
|
artifact: content.slice(0, 200),
|
|
19414
|
-
sourceFile: (0,
|
|
20038
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
|
|
19415
20039
|
});
|
|
19416
20040
|
}
|
|
19417
20041
|
} catch {
|
|
@@ -19421,7 +20045,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19421
20045
|
allUrls.push(...extractUrls(content));
|
|
19422
20046
|
}
|
|
19423
20047
|
if (checkFileHashes) {
|
|
19424
|
-
const sha256 = (0,
|
|
20048
|
+
const sha256 = (0, import_node_crypto9.createHash)("sha256").update(rawBytes).digest("hex");
|
|
19425
20049
|
const existing = hashToFiles.get(sha256);
|
|
19426
20050
|
if (existing) {
|
|
19427
20051
|
existing.push(filePath);
|
|
@@ -19442,8 +20066,6 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19442
20066
|
threatId: "URL_CHECK",
|
|
19443
20067
|
title: `Malicious URL (${findingDetails})`,
|
|
19444
20068
|
severity: "critical",
|
|
19445
|
-
confidence: 1,
|
|
19446
|
-
action: "block",
|
|
19447
20069
|
artifact: ur.url.slice(0, 200),
|
|
19448
20070
|
sourceFile: "URL check"
|
|
19449
20071
|
});
|
|
@@ -19465,10 +20087,8 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19465
20087
|
threatId: "FILE_CHECK",
|
|
19466
20088
|
title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
|
|
19467
20089
|
severity: "critical",
|
|
19468
|
-
confidence: 1,
|
|
19469
|
-
action: "block",
|
|
19470
20090
|
artifact: fr.sha256,
|
|
19471
|
-
sourceFile: (0,
|
|
20091
|
+
sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
|
|
19472
20092
|
});
|
|
19473
20093
|
}
|
|
19474
20094
|
}
|
|
@@ -19476,48 +20096,109 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
19476
20096
|
} catch {
|
|
19477
20097
|
}
|
|
19478
20098
|
})() : Promise.resolve();
|
|
19479
|
-
await Promise.all([
|
|
20099
|
+
const [, , skillDeferCache] = await Promise.all([
|
|
20100
|
+
urlCheckPromise,
|
|
20101
|
+
fileCheckPromise,
|
|
20102
|
+
skillCheckPromise
|
|
20103
|
+
]);
|
|
20104
|
+
if (skillDeferCache)
|
|
20105
|
+
result.deferCache = true;
|
|
19480
20106
|
return result;
|
|
19481
20107
|
}
|
|
19482
|
-
|
|
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 = {}) {
|
|
19483
20122
|
try {
|
|
19484
|
-
const skills = await computeSkillIdsForRoot(plugin.installPath);
|
|
20123
|
+
const skills = await computeSkillIdsForRoot(plugin.installPath, logger);
|
|
19485
20124
|
if (skills.length === 0)
|
|
19486
|
-
return;
|
|
19487
|
-
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;
|
|
19488
20138
|
const client = new SkillCheckClient(void 0, logger);
|
|
19489
|
-
const verdicts = await client.checkSkills(
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
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;
|
|
19496
20148
|
continue;
|
|
19497
|
-
|
|
19498
|
-
|
|
19499
|
-
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19505
|
-
|
|
19506
|
-
|
|
19507
|
-
|
|
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
|
+
}
|
|
19508
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;
|
|
19509
20187
|
} catch (e) {
|
|
19510
20188
|
logger.debug("Skill check failed", { error: String(e) });
|
|
20189
|
+
return false;
|
|
19511
20190
|
}
|
|
19512
20191
|
}
|
|
19513
20192
|
|
|
19514
20193
|
// ../core/dist/session-start.js
|
|
19515
20194
|
var import_node_child_process2 = require("node:child_process");
|
|
19516
20195
|
var import_node_fs4 = require("node:fs");
|
|
20196
|
+
var import_node_path25 = require("node:path");
|
|
19517
20197
|
init_config();
|
|
19518
20198
|
init_file_utils();
|
|
19519
20199
|
|
|
19520
20200
|
// ../core/dist/session-start-scan.js
|
|
20201
|
+
var import_node_path24 = require("node:path");
|
|
19521
20202
|
init_config();
|
|
19522
20203
|
init_types2();
|
|
19523
20204
|
function fromCachedFinding(finding) {
|
|
@@ -19525,8 +20206,6 @@ function fromCachedFinding(finding) {
|
|
|
19525
20206
|
threatId: finding.threat_id,
|
|
19526
20207
|
title: finding.title,
|
|
19527
20208
|
severity: finding.severity,
|
|
19528
|
-
confidence: finding.confidence,
|
|
19529
|
-
action: finding.action,
|
|
19530
20209
|
artifact: finding.artifact,
|
|
19531
20210
|
sourceFile: finding.source_file,
|
|
19532
20211
|
recommendations: finding.recommendations
|
|
@@ -19537,16 +20216,13 @@ function toFindingData(finding) {
|
|
|
19537
20216
|
threat_id: finding.threatId,
|
|
19538
20217
|
title: finding.title,
|
|
19539
20218
|
severity: finding.severity,
|
|
19540
|
-
confidence: finding.confidence,
|
|
19541
|
-
action: finding.action,
|
|
19542
20219
|
artifact: finding.artifact,
|
|
19543
20220
|
source_file: finding.sourceFile,
|
|
19544
20221
|
recommendations: finding.recommendations
|
|
19545
20222
|
};
|
|
19546
20223
|
}
|
|
19547
20224
|
function toAuditFindingData(finding) {
|
|
19548
|
-
|
|
19549
|
-
return rest;
|
|
20225
|
+
return { ...toFindingData(finding) };
|
|
19550
20226
|
}
|
|
19551
20227
|
async function runSessionStartScan(context) {
|
|
19552
20228
|
const logger = context.logger ?? nullLogger;
|
|
@@ -19591,7 +20267,7 @@ async function runSessionStartScan(context) {
|
|
|
19591
20267
|
}
|
|
19592
20268
|
}
|
|
19593
20269
|
async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient, logger) {
|
|
19594
|
-
const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.
|
|
20270
|
+
const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.trustedDomainsDir);
|
|
19595
20271
|
const cache2 = await loadScanCache(configHash, context.scanCachePath, logger);
|
|
19596
20272
|
const resultsWithFindings = [];
|
|
19597
20273
|
let cacheModified = false;
|
|
@@ -19605,6 +20281,9 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19605
20281
|
deniedByException: 0,
|
|
19606
20282
|
findingsCount: 0
|
|
19607
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);
|
|
19608
20287
|
for (const plugin of plugins) {
|
|
19609
20288
|
const denyMatch = findPluginDenyException(exceptions, plugin.key);
|
|
19610
20289
|
if (denyMatch) {
|
|
@@ -19617,8 +20296,6 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19617
20296
|
threatId: "EXCEPTION-DENY",
|
|
19618
20297
|
title: `Denied by exception: ${denyMatch.pattern}${denyMatch.reason ? ` \u2014 ${denyMatch.reason}` : ""}`,
|
|
19619
20298
|
severity: "critical",
|
|
19620
|
-
confidence: 1,
|
|
19621
|
-
action: "block",
|
|
19622
20299
|
artifact: plugin.key,
|
|
19623
20300
|
sourceFile: "~/.sage/exceptions.json"
|
|
19624
20301
|
}
|
|
@@ -19631,7 +20308,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19631
20308
|
stats.allowedByException += 1;
|
|
19632
20309
|
continue;
|
|
19633
20310
|
}
|
|
19634
|
-
const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
|
|
20311
|
+
const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated, cacheTtlMs);
|
|
19635
20312
|
if (cached && cached.findings.length === 0) {
|
|
19636
20313
|
stats.cacheHitsClean += 1;
|
|
19637
20314
|
continue;
|
|
@@ -19650,11 +20327,23 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19650
20327
|
const result = await scanPlugin(plugin, {
|
|
19651
20328
|
checkUrls: context.checkUrls ?? true,
|
|
19652
20329
|
checkFileHashes: context.checkFileHashes ?? true,
|
|
20330
|
+
checkSkills: config2.skill_check.enabled,
|
|
20331
|
+
uploadEnabled: context.uploadUnknownSkills ?? config2.skill_check.upload_enabled,
|
|
20332
|
+
deferUnknownSkills: context.deferUnknownSkills,
|
|
19653
20333
|
amsiClient,
|
|
19654
|
-
logger
|
|
20334
|
+
logger,
|
|
20335
|
+
skillPendingPath,
|
|
20336
|
+
skillVerdictCachePath,
|
|
20337
|
+
skillVerdictTtlMs: cacheTtlMs,
|
|
20338
|
+
loggingConfig: config2.logging,
|
|
20339
|
+
agentRuntime: context.agentRuntime
|
|
19655
20340
|
});
|
|
19656
|
-
|
|
19657
|
-
|
|
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
|
+
}
|
|
19658
20347
|
if (result.findings.length > 0) {
|
|
19659
20348
|
stats.findingsCount += result.findings.length;
|
|
19660
20349
|
resultsWithFindings.push(result);
|
|
@@ -19681,6 +20370,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
19681
20370
|
init_types2();
|
|
19682
20371
|
|
|
19683
20372
|
// ../core/dist/version-check.js
|
|
20373
|
+
init_config();
|
|
19684
20374
|
init_types2();
|
|
19685
20375
|
var DEFAULT_TIMEOUT_MS3 = 5e3;
|
|
19686
20376
|
function isNewerVersion(current, latest) {
|
|
@@ -19707,11 +20397,20 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
|
|
|
19707
20397
|
logger.debug("Skipping version check: missing installation id");
|
|
19708
20398
|
return null;
|
|
19709
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
|
+
}
|
|
19710
20408
|
const envelope = buildSageProxyEnvelope({
|
|
19711
20409
|
iid: context.iid,
|
|
19712
20410
|
versionApp: currentVersion,
|
|
19713
20411
|
agentRuntime: context.agentRuntime,
|
|
19714
|
-
agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown"
|
|
20412
|
+
agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown",
|
|
20413
|
+
config: config2
|
|
19715
20414
|
});
|
|
19716
20415
|
const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
|
|
19717
20416
|
const enriched = mergeExtendedInfo(envelope, extendedInfo);
|
|
@@ -19751,6 +20450,8 @@ async function runSessionStart(ctx) {
|
|
|
19751
20450
|
const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
|
|
19752
20451
|
pruneOrphanedTmpFiles(sageDirPath).catch(() => {
|
|
19753
20452
|
});
|
|
20453
|
+
deployConfigDefaults(sageDirPath, logger).catch(() => {
|
|
20454
|
+
});
|
|
19754
20455
|
maybeSpawnModelDownloadWorker({
|
|
19755
20456
|
sageDirPath,
|
|
19756
20457
|
workerPath: ctx.modelDownloadWorkerPath,
|
|
@@ -19761,18 +20462,29 @@ async function runSessionStart(ctx) {
|
|
|
19761
20462
|
logger
|
|
19762
20463
|
}).catch(() => {
|
|
19763
20464
|
});
|
|
20465
|
+
const rollout = await resolveSkillUploadRollout({
|
|
20466
|
+
sageDirPath,
|
|
20467
|
+
configPath: ctx.configPath,
|
|
20468
|
+
version: ctx.version,
|
|
20469
|
+
logger,
|
|
20470
|
+
commitNotice: ctx.commitUploadNotice ?? true
|
|
20471
|
+
});
|
|
19764
20472
|
const iidPromise = getInstallationId(sageDirPath).catch(() => void 0);
|
|
19765
20473
|
const [scanResults, versionCheck] = await Promise.all([
|
|
19766
20474
|
runSessionStartScan({
|
|
19767
20475
|
plugins: ctx.plugins,
|
|
19768
20476
|
threatsDir: ctx.threatsDir,
|
|
19769
|
-
|
|
20477
|
+
trustedDomainsDir: ctx.trustedDomainsDir,
|
|
19770
20478
|
sageVersion: ctx.version,
|
|
19771
20479
|
logger,
|
|
19772
20480
|
configPath: ctx.configPath,
|
|
19773
20481
|
scanCachePath: ctx.scanCachePath,
|
|
19774
20482
|
checkUrls: ctx.checkUrls,
|
|
19775
|
-
checkFileHashes: ctx.checkFileHashes
|
|
20483
|
+
checkFileHashes: ctx.checkFileHashes,
|
|
20484
|
+
sageDirPath,
|
|
20485
|
+
agentRuntime: ctx.agentRuntime,
|
|
20486
|
+
uploadUnknownSkills: rollout.uploadActive,
|
|
20487
|
+
deferUnknownSkills: rollout.deferUnknownSkills
|
|
19776
20488
|
}),
|
|
19777
20489
|
iidPromise.then((iid) => {
|
|
19778
20490
|
if (!iid) {
|
|
@@ -19781,11 +20493,25 @@ async function runSessionStart(ctx) {
|
|
|
19781
20493
|
return checkForUpdate(ctx.version, logger, void 0, {
|
|
19782
20494
|
agentRuntime: ctx.agentRuntime,
|
|
19783
20495
|
agentRuntimeVersion: ctx.agentRuntimeVersion,
|
|
19784
|
-
iid
|
|
20496
|
+
iid,
|
|
20497
|
+
configPath: ctx.configPath
|
|
19785
20498
|
});
|
|
19786
20499
|
}).catch(() => null)
|
|
19787
20500
|
]);
|
|
19788
|
-
|
|
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
|
+
};
|
|
19789
20515
|
}
|
|
19790
20516
|
async function maybeSpawnModelDownloadWorker(args) {
|
|
19791
20517
|
if (!args.workerPath) {
|
|
@@ -19831,6 +20557,7 @@ async function maybeSpawnModelDownloadWorker(args) {
|
|
|
19831
20557
|
const spawned = spawnModelDownloadWorker({
|
|
19832
20558
|
sageDirPath: args.sageDirPath,
|
|
19833
20559
|
workerPath: args.workerPath,
|
|
20560
|
+
configPath: args.configPath,
|
|
19834
20561
|
agentRuntime: args.agentRuntime,
|
|
19835
20562
|
agentRuntimeVersion: args.agentRuntimeVersion,
|
|
19836
20563
|
versionApp: args.versionApp,
|
|
@@ -19842,6 +20569,9 @@ async function maybeSpawnModelDownloadWorker(args) {
|
|
|
19842
20569
|
missingModels
|
|
19843
20570
|
});
|
|
19844
20571
|
}
|
|
20572
|
+
function bunRuntimeEnv() {
|
|
20573
|
+
return process.versions.bun ? { BUN_BE_BUN: "1" } : {};
|
|
20574
|
+
}
|
|
19845
20575
|
function spawnModelDownloadWorker(args) {
|
|
19846
20576
|
const logger = args.logger ?? nullLogger;
|
|
19847
20577
|
try {
|
|
@@ -19850,7 +20580,9 @@ function spawnModelDownloadWorker(args) {
|
|
|
19850
20580
|
stdio: "ignore",
|
|
19851
20581
|
env: {
|
|
19852
20582
|
...process.env,
|
|
20583
|
+
...bunRuntimeEnv(),
|
|
19853
20584
|
SAGE_DIR: args.sageDirPath,
|
|
20585
|
+
SAGE_CONFIG_PATH: args.configPath ?? "",
|
|
19854
20586
|
SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown"),
|
|
19855
20587
|
SAGE_AGENT_RUNTIME_VERSION: args.agentRuntimeVersion ?? "",
|
|
19856
20588
|
SAGE_VERSION_APP: args.versionApp ?? "",
|
|
@@ -19864,24 +20596,111 @@ function spawnModelDownloadWorker(args) {
|
|
|
19864
20596
|
return false;
|
|
19865
20597
|
}
|
|
19866
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
|
+
}
|
|
19867
20673
|
|
|
19868
20674
|
// ../core/dist/scan-handler.js
|
|
19869
|
-
async function runPluginScan(logger, context, plugins, threatsDir,
|
|
20675
|
+
async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans = true) {
|
|
19870
20676
|
logger.debug(`${branding2.name} plugin scan started (${context})`, {
|
|
19871
20677
|
agentRuntime,
|
|
19872
20678
|
pluginsCount: plugins.length,
|
|
19873
20679
|
threatsDir,
|
|
19874
|
-
|
|
20680
|
+
trustedDomainsDir
|
|
19875
20681
|
});
|
|
19876
20682
|
const result = await runSessionStart({
|
|
19877
20683
|
plugins,
|
|
19878
20684
|
threatsDir,
|
|
19879
|
-
|
|
20685
|
+
trustedDomainsDir,
|
|
19880
20686
|
version,
|
|
19881
20687
|
logger,
|
|
19882
20688
|
agentRuntime,
|
|
19883
|
-
|
|
20689
|
+
agentRuntimeVersion,
|
|
20690
|
+
modelDownloadWorkerPath,
|
|
20691
|
+
skillUploadWorkerPath,
|
|
20692
|
+
commitUploadNotice
|
|
19884
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
|
+
}
|
|
19885
20704
|
const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
|
|
19886
20705
|
const completionData = {
|
|
19887
20706
|
agentRuntime,
|
|
@@ -19895,10 +20714,13 @@ async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir
|
|
|
19895
20714
|
} else {
|
|
19896
20715
|
logger.debug(`${branding2.name} plugin scan (${context}) complete`, completionData);
|
|
19897
20716
|
}
|
|
20717
|
+
if (findingsCount === 0 && !announceCleanScans) {
|
|
20718
|
+
return "";
|
|
20719
|
+
}
|
|
19898
20720
|
return formatSessionStartMessage(version, result, branding2, style);
|
|
19899
20721
|
}
|
|
19900
20722
|
function createScanHandler(options) {
|
|
19901
|
-
const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir,
|
|
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;
|
|
19902
20724
|
return async () => {
|
|
19903
20725
|
try {
|
|
19904
20726
|
let plugins = await discoverPlugins2();
|
|
@@ -19906,8 +20728,10 @@ function createScanHandler(options) {
|
|
|
19906
20728
|
if (plugins.length === 0) {
|
|
19907
20729
|
logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
|
|
19908
20730
|
}
|
|
19909
|
-
const msg = await runPluginScan(logger, context, plugins, threatsDir,
|
|
19910
|
-
|
|
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
|
+
}
|
|
19911
20735
|
} catch (e) {
|
|
19912
20736
|
logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
|
|
19913
20737
|
}
|
|
@@ -19945,19 +20769,19 @@ function canonicalizeToolName(map, rawName) {
|
|
|
19945
20769
|
init_types2();
|
|
19946
20770
|
|
|
19947
20771
|
// src/bundled-dirs.ts
|
|
19948
|
-
var
|
|
19949
|
-
var packageRoot = (0,
|
|
20772
|
+
var import_node_path26 = require("node:path");
|
|
20773
|
+
var packageRoot = (0, import_node_path26.resolve)(__dirname, "..");
|
|
19950
20774
|
function getBundledDataDirs() {
|
|
19951
20775
|
return {
|
|
19952
|
-
threatsDir: (0,
|
|
19953
|
-
|
|
20776
|
+
threatsDir: (0, import_node_path26.join)(packageRoot, "resources", "threats"),
|
|
20777
|
+
trustedDomainsDir: (0, import_node_path26.join)(packageRoot, "resources", "trusted-domains")
|
|
19954
20778
|
};
|
|
19955
20779
|
}
|
|
19956
20780
|
var cachedVersion;
|
|
19957
20781
|
function getSageVersion() {
|
|
19958
20782
|
if (cachedVersion) return cachedVersion;
|
|
19959
20783
|
try {
|
|
19960
|
-
const pkg = JSON.parse(getFileContentSync((0,
|
|
20784
|
+
const pkg = JSON.parse(getFileContentSync((0, import_node_path26.join)(packageRoot, "package.json"), "utf-8"));
|
|
19961
20785
|
cachedVersion = pkg.version ?? "0.0.0";
|
|
19962
20786
|
} catch {
|
|
19963
20787
|
cachedVersion = "0.0.0";
|
|
@@ -19966,37 +20790,41 @@ function getSageVersion() {
|
|
|
19966
20790
|
}
|
|
19967
20791
|
|
|
19968
20792
|
// src/startup-scan.ts
|
|
19969
|
-
var
|
|
20793
|
+
var import_node_path28 = require("node:path");
|
|
19970
20794
|
|
|
19971
20795
|
// src/plugin-discovery.ts
|
|
19972
|
-
var
|
|
20796
|
+
var import_promises12 = require("node:fs/promises");
|
|
19973
20797
|
var import_node_os4 = require("node:os");
|
|
19974
|
-
var
|
|
19975
|
-
var DEFAULT_EXTENSIONS_DIR = (0,
|
|
19976
|
-
|
|
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) {
|
|
19977
20805
|
logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
|
|
19978
20806
|
path: extensionsDir
|
|
19979
20807
|
});
|
|
20808
|
+
const plugins = [];
|
|
19980
20809
|
let entries;
|
|
19981
20810
|
try {
|
|
19982
|
-
entries = await (0,
|
|
20811
|
+
entries = await (0, import_promises12.readdir)(extensionsDir);
|
|
19983
20812
|
} catch {
|
|
19984
20813
|
logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
|
|
19985
|
-
|
|
20814
|
+
entries = [];
|
|
19986
20815
|
}
|
|
19987
|
-
const plugins = [];
|
|
19988
20816
|
for (const entry of entries) {
|
|
19989
|
-
const extDir = (0,
|
|
20817
|
+
const extDir = (0, import_node_path27.join)(extensionsDir, entry);
|
|
19990
20818
|
let stats;
|
|
19991
20819
|
try {
|
|
19992
|
-
stats = await (0,
|
|
20820
|
+
stats = await (0, import_promises12.stat)(extDir);
|
|
19993
20821
|
if (!stats.isDirectory()) continue;
|
|
19994
20822
|
} catch {
|
|
19995
20823
|
continue;
|
|
19996
20824
|
}
|
|
19997
20825
|
let pkg;
|
|
19998
20826
|
try {
|
|
19999
|
-
const pkgPath = (0,
|
|
20827
|
+
const pkgPath = (0, import_node_path27.join)(extDir, "package.json");
|
|
20000
20828
|
const raw = await getFileContent(pkgPath);
|
|
20001
20829
|
pkg = JSON.parse(raw);
|
|
20002
20830
|
} catch {
|
|
@@ -20018,12 +20846,15 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
20018
20846
|
});
|
|
20019
20847
|
}
|
|
20020
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);
|
|
20021
20852
|
return plugins;
|
|
20022
20853
|
}
|
|
20023
20854
|
|
|
20024
20855
|
// src/startup-scan.ts
|
|
20025
|
-
function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult) {
|
|
20026
|
-
const { threatsDir,
|
|
20856
|
+
function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult, announceCleanScans = true) {
|
|
20857
|
+
const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
|
|
20027
20858
|
const version = getSageVersion();
|
|
20028
20859
|
return createScanHandler({
|
|
20029
20860
|
logger,
|
|
@@ -20031,34 +20862,64 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
|
|
|
20031
20862
|
discoverPlugins: () => discoverOpenClawPlugins(logger, void 0, branding2),
|
|
20032
20863
|
selfPrefix: "@gendigital/sage-openclaw@",
|
|
20033
20864
|
threatsDir,
|
|
20034
|
-
|
|
20865
|
+
trustedDomainsDir,
|
|
20035
20866
|
version,
|
|
20036
20867
|
agentRuntime: "openclaw",
|
|
20868
|
+
// No agentRuntimeVersion: the OpenClaw plugin API exposes no host version.
|
|
20037
20869
|
branding: branding2,
|
|
20038
20870
|
onResult,
|
|
20039
|
-
|
|
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
|
|
20040
20879
|
});
|
|
20041
20880
|
}
|
|
20042
|
-
function createStartupScanHandler(logger, branding2 = defaultBranding, onResult) {
|
|
20043
|
-
return createOpenClawScanHandler(logger, "startup", branding2, onResult);
|
|
20044
|
-
}
|
|
20045
|
-
function createSessionScanHandler(logger, branding2 = defaultBranding, onResult) {
|
|
20046
|
-
return createOpenClawScanHandler(logger, "session", branding2, onResult);
|
|
20047
|
-
}
|
|
20048
|
-
function createBeforeAgentStartHandler(
|
|
20049
|
-
return () => {
|
|
20050
|
-
const findings =
|
|
20051
|
-
if (!findings)
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
]
|
|
20061
|
-
|
|
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") };
|
|
20062
20923
|
};
|
|
20063
20924
|
}
|
|
20064
20925
|
|
|
@@ -20124,7 +20985,7 @@ function mapToolToArtifacts(toolName, params) {
|
|
|
20124
20985
|
return null;
|
|
20125
20986
|
}
|
|
20126
20987
|
}
|
|
20127
|
-
function createToolCallHandler(approvalStore, logger, threatsDir,
|
|
20988
|
+
function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomainsDir, branding2 = defaultBranding) {
|
|
20128
20989
|
async function handleToolCall(event, ctx) {
|
|
20129
20990
|
const sessionId = ctx?.sessionKey ?? "unknown";
|
|
20130
20991
|
const completeHook = (result, data = {}) => {
|
|
@@ -20145,6 +21006,17 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
|
|
|
20145
21006
|
});
|
|
20146
21007
|
try {
|
|
20147
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
|
+
}
|
|
20148
21020
|
const artifacts = mapToolToArtifacts(toolName, params);
|
|
20149
21021
|
if (!artifacts || artifacts.length === 0) {
|
|
20150
21022
|
completeHook("skipped", { skippedReason: "no_artifacts" });
|
|
@@ -20160,7 +21032,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
|
|
|
20160
21032
|
toolInput: normalizeToolInput(toolName, params),
|
|
20161
21033
|
artifacts
|
|
20162
21034
|
},
|
|
20163
|
-
{ threatsDir,
|
|
21035
|
+
{ threatsDir, trustedDomainsDir, logger },
|
|
20164
21036
|
approvalStore
|
|
20165
21037
|
);
|
|
20166
21038
|
if (verdict.decision === "allow") {
|
|
@@ -20183,7 +21055,8 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
|
|
|
20183
21055
|
});
|
|
20184
21056
|
return { block: true, blockReason: formatDenyMessage(verdict, branding2) };
|
|
20185
21057
|
}
|
|
20186
|
-
const
|
|
21058
|
+
const maxReasons = 3;
|
|
21059
|
+
const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, maxReasons).join("; ") + (verdict.reasons.length > maxReasons ? `; ... and ${verdict.reasons.length - maxReasons} more` : "") : verdict.category;
|
|
20187
21060
|
const intersected = artifacts.filter((a) => verdict.artifacts.includes(a.value));
|
|
20188
21061
|
const flaggedArtifacts = intersected.length > 0 ? intersected : artifacts;
|
|
20189
21062
|
completeHook("evaluated", {
|
|
@@ -20251,35 +21124,66 @@ var index_default = {
|
|
|
20251
21124
|
const toolLogger = operationalLogger.forComponent("tool-handler");
|
|
20252
21125
|
const scanLogger = operationalLogger.forComponent("startup-scan");
|
|
20253
21126
|
const approvalStore = new ApprovalStore();
|
|
20254
|
-
const { threatsDir,
|
|
21127
|
+
const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
|
|
20255
21128
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
20256
21129
|
const interval = setInterval(() => approvalStore.cleanup(), CLEANUP_INTERVAL_MS);
|
|
20257
21130
|
if (typeof interval.unref === "function") interval.unref();
|
|
20258
21131
|
let pendingConfigurationWarnings = formatConfigurationWarnings(getConfigurationWarningsSync(void 0, logger), branding) ?? null;
|
|
21132
|
+
const migrationCheckPromise = checkAllowlistMigration().then((result) => {
|
|
21133
|
+
if (result.needed) {
|
|
21134
|
+
const notice = formatAllowlistMigrationWarning(result.entryTypes, branding);
|
|
21135
|
+
pendingConfigurationWarnings = pendingConfigurationWarnings ? `${pendingConfigurationWarnings}
|
|
21136
|
+
|
|
21137
|
+
${notice}` : notice;
|
|
21138
|
+
}
|
|
21139
|
+
}).catch(() => {
|
|
21140
|
+
});
|
|
20259
21141
|
let pendingScanFindings = null;
|
|
20260
21142
|
const onFindings = (msg) => {
|
|
20261
21143
|
pendingScanFindings = msg;
|
|
20262
21144
|
};
|
|
20263
|
-
const
|
|
21145
|
+
const getPendingSecurityFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
|
|
21146
|
+
const beforeAgentStartHandler = createBeforeAgentStartHandler(
|
|
21147
|
+
getPendingSecurityFindings,
|
|
21148
|
+
() => {
|
|
21149
|
+
pendingConfigurationWarnings = null;
|
|
21150
|
+
pendingScanFindings = null;
|
|
21151
|
+
},
|
|
21152
|
+
logger,
|
|
21153
|
+
branding
|
|
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
|
+
};
|
|
20264
21169
|
api.on(
|
|
20265
21170
|
"before_tool_call",
|
|
20266
|
-
createToolCallHandler(approvalStore, toolLogger, threatsDir,
|
|
21171
|
+
createToolCallHandler(approvalStore, toolLogger, threatsDir, trustedDomainsDir, branding),
|
|
20267
21172
|
{ priority: 100 }
|
|
20268
21173
|
);
|
|
20269
|
-
api.on("gateway_start", createStartupScanHandler(scanLogger, branding, onFindings));
|
|
20270
|
-
api.on("session_start", createSessionScanHandler(scanLogger, branding, onFindings));
|
|
20271
21174
|
api.on(
|
|
20272
|
-
"
|
|
20273
|
-
|
|
20274
|
-
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
},
|
|
20279
|
-
logger,
|
|
20280
|
-
branding
|
|
20281
|
-
)
|
|
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)
|
|
20282
21181
|
);
|
|
21182
|
+
api.on("before_agent_start", async () => {
|
|
21183
|
+
await migrationCheckPromise;
|
|
21184
|
+
const notices = await resolveNoticeText();
|
|
21185
|
+
return beforeAgentStartHandler(notices);
|
|
21186
|
+
});
|
|
20283
21187
|
}
|
|
20284
21188
|
};
|
|
20285
21189
|
//# sourceMappingURL=index.cjs.map
|