@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.
Files changed (37) hide show
  1. package/README.md +5 -5
  2. package/dist/index.cjs +1881 -977
  3. package/dist/index.cjs.map +4 -4
  4. package/dist/mcp-server.cjs +31882 -0
  5. package/dist/mcp-server.cjs.map +7 -0
  6. package/dist/model-download-worker.cjs +140 -111
  7. package/dist/model-download-worker.cjs.map +3 -3
  8. package/dist/skill-upload-worker.cjs +5632 -0
  9. package/dist/skill-upload-worker.cjs.map +7 -0
  10. package/openclaw.plugin.json +6 -1
  11. package/package.json +9 -3
  12. package/resources/skills/security-awareness/SKILL.md +5 -1
  13. package/resources/threats/agent-layer.yaml +186 -0
  14. package/resources/threats/commands.yaml +24 -53
  15. package/resources/threats/credentials.yaml +12 -20
  16. package/resources/threats/dummy.yaml +44 -50
  17. package/resources/threats/files.yaml +34 -53
  18. package/resources/threats/mac-commands.yaml +20 -47
  19. package/resources/threats/mac-credentials.yaml +4 -12
  20. package/resources/threats/mac-files.yaml +5 -15
  21. package/resources/threats/mac-obfuscation.yaml +6 -13
  22. package/resources/threats/mac-persistence.yaml +11 -20
  23. package/resources/threats/mac-supply-chain.yaml +5 -8
  24. package/resources/threats/mitre.yaml +105 -167
  25. package/resources/threats/obfuscation.yaml +7 -15
  26. package/resources/threats/persistence.yaml +10 -17
  27. package/resources/threats/prompt-injection.yaml +92 -43
  28. package/resources/threats/self-defense.yaml +8 -12
  29. package/resources/threats/supply_chain.yaml +1 -2
  30. package/resources/threats/urls.yaml +9 -14
  31. package/resources/threats/win-commands.yaml +46 -106
  32. package/resources/threats/win-credentials.yaml +14 -24
  33. package/resources/threats/win-files.yaml +10 -20
  34. package/resources/threats/win-obfuscation.yaml +21 -37
  35. package/resources/threats/win-persistence.yaml +8 -16
  36. package/resources/threats/win-supply-chain.yaml +2 -3
  37. /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 fsPromises.rename(tmp, path);
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
- var import_node_crypto, fs, fsPromises, import_node_os, import_node_path, name1, name2;
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, SeveritySchema, ActionSchema, ThreatSchema, DecisionSchema, VerdictSeveritySchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, AllowlistConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4295
+ var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, SkillCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, DEFAULT_PI_TELEMETRY_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
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
- SeveritySchema = external_exports.enum(["critical", "high", "medium", "low"]);
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: SeveritySchema,
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
- resolvePath: () => resolvePath
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 \u2014 ignoring`, { brand_key: brandKey });
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((resolve9, reject) => {
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
- resolve9();
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
- init_file_utils();
14127
- function normalizeUrl(raw) {
14128
- try {
14129
- const u = new URL(raw);
14130
- u.hash = "";
14131
- u.searchParams.sort();
14132
- return u.toString();
14133
- } catch {
14134
- return raw.toLowerCase();
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
- function emptyAllowlist() {
14148
- return { urls: {}, commands: {}, filePaths: {} };
14149
- }
14150
- function parseEntries(raw) {
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 loadAllowlist(config2, logger = nullLogger) {
14167
- const path = resolvePath(config2.path);
14168
- let raw;
14238
+ async function checkAllowlistMigration() {
14169
14239
  try {
14170
- raw = await getFileContent(path);
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 emptyAllowlist();
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 isAllowlisted(allowlist, artifacts) {
14203
- for (const artifact of artifacts) {
14204
- if (artifact.type !== "command") {
14205
- continue;
14206
- }
14207
- const cmdHash = hashCommand(artifact.value);
14208
- if (cmdHash in allowlist.commands) {
14209
- return true;
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 import_node_crypto3 = require("node:crypto");
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 _ApprovalStore {
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, import_node_crypto3.createHash)("sha256").update(payload).digest("hex").slice(0, 32);
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 import_node_crypto4 = require("node:crypto");
14324
+ var import_node_crypto3 = require("node:crypto");
14322
14325
  init_config();
14323
- init_file_utils();
14324
14326
 
14325
- // ../core/dist/jsonl-log-writer.js
14326
- var import_promises = require("node:fs/promises");
14327
- var import_node_path4 = require("node:path");
14328
- var import_promises2 = require("node:timers/promises");
14329
- init_config();
14330
- var writeQueues = /* @__PURE__ */ new Map();
14331
- var ROTATE_LOCK_TIMEOUT_MS = 250;
14332
- var ROTATE_LOCK_STALE_MS = 3e4;
14333
- var ROTATE_LOCK_POLL_MS = 50;
14334
- async function shouldRotate(filePath, maxBytes, maxFiles) {
14335
- if (maxBytes <= 0 || maxFiles <= 0)
14336
- return false;
14337
- try {
14338
- const s = await (0, import_promises.stat)(filePath);
14339
- return s.size >= maxBytes;
14340
- } catch {
14341
- return false;
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
- async function removeStaleRotateLock(lockPath) {
14345
- try {
14346
- const s = await (0, import_promises.stat)(lockPath);
14347
- if (Date.now() - s.mtimeMs < ROTATE_LOCK_STALE_MS)
14348
- return;
14349
- await (0, import_promises.rmdir)(lockPath);
14350
- } catch {
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
- async function acquireRotateLock(filePath) {
14354
- const lockPath = `${filePath}.lock`;
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
- async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
14375
- if (!await shouldRotate(filePath, maxBytes, maxFiles))
14376
- return;
14377
- const releaseLock = await acquireRotateLock(filePath);
14378
- if (!releaseLock)
14379
- return;
14380
- try {
14381
- if (!await shouldRotate(filePath, maxBytes, maxFiles))
14382
- return;
14383
- try {
14384
- await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
14385
- } catch {
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
- async function appendJsonlEntryNow(path, config2, entry) {
14402
- await (0, import_promises.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
14403
- await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
14404
- await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
14405
- `);
14406
- }
14407
- async function appendJsonlEntry(config2, entry) {
14408
- const path = resolvePath(config2.path);
14409
- const previousWrite = writeQueues.get(path) ?? Promise.resolve();
14410
- const nextWrite = previousWrite.catch(() => {
14411
- }).then(() => appendJsonlEntryNow(path, config2, entry));
14412
- writeQueues.set(path, nextWrite);
14413
- try {
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, import_node_crypto4.randomUUID)(),
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
- // ../core/dist/brands.js
14492
- var defaultBranding = { name: "Sage", short_name: "Sage" };
14493
- var BRANDS = {
14494
- norton: { name: "Norton AI Agent Protection", short_name: "Norton" }
14495
- };
14496
- function resolveBranding(brandKey, logger) {
14497
- if (!brandKey)
14498
- return defaultBranding;
14499
- const entry = BRANDS[brandKey];
14500
- if (!entry) {
14501
- logger?.warn(`Unknown brand_key "${brandKey}" in config \u2014 using default branding`);
14502
- return defaultBranding;
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: {}, commands: {}, packages: {} };
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: {}, commands: {}, packages: {} };
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((resolve9, reject) => {
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: resolve9, reject, timer };
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: resolve9, timer } = this.pendingResponse;
15146
+ const { resolve: resolve10, timer } = this.pendingResponse;
15005
15147
  this.pendingResponse = null;
15006
15148
  clearTimeout(timer);
15007
- resolve9(line);
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
- const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
15038
- try {
15039
- const req = JSON.stringify({ content: truncated, contentName });
15040
- this.process.stdin?.write(`${req}
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
- const line = await this.waitForLine(PS_TIMEOUT);
15043
- const amsiResult = parseInt(line, 10);
15044
- if (Number.isNaN(amsiResult) || amsiResult < 0) {
15045
- if (!this.invalidResultLogged) {
15046
- this.invalidResultLogged = true;
15047
- this.logger.warn("AMSI: PowerShell scan returned invalid result", {
15048
- stdout: line.slice(0, 100),
15049
- contentName
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
- this.logger.debug("AMSI: PowerShell scan result", { contentName, amsiResult });
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((resolve9) => {
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
- resolve9(null);
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
- resolve9(null);
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
- resolve9(null);
15284
+ resolve10(null);
15137
15285
  return;
15138
15286
  }
15139
15287
  this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
15140
- resolve9(interpretAmsiResult(amsiResult, content, contentName));
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
- resolve9(null);
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.10.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 = "v1";
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 mapSageProxyOs(platform) {
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 mapSageProxyArchitecture(arch) {
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 ?? mapSageProxyOs(process.platform),
15501
- architecture: args.platformArchitecture ?? mapSageProxyArchitecture(process.arch)
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. A `null` value means the proxy had
16031
- * no opinion on that skill (analogous to a clean verdict). Skill ids
16032
- * not found in the response are simply absent from the map.
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
- overallRiskLevel: typeof raw.overall_risk_level === "string" ? raw.overall_risk_level : void 0,
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 import_promises3 = require("node:fs/promises");
16487
+ var import_promises4 = require("node:fs/promises");
16422
16488
  var import_node_os3 = require("node:os");
16423
- var import_node_path10 = require("node:path");
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, import_node_path10.join)((0, import_node_os3.homedir)(), ".sage");
16490
- const filePath = (0, import_node_path10.join)(sageDir, EXTENDED_INFO_FILENAME);
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, import_promises3.stat)(filePath);
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 import_node_crypto5 = require("node:crypto");
16589
- var import_promises4 = require("node:fs/promises");
16590
- var import_node_path11 = require("node:path");
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, import_node_path11.join)(sageDir, "installation-id");
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, import_node_crypto5.randomUUID)();
16607
- await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
16608
- await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
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 sendCommunityIqDetection(args) {
16703
+ async function sendCommunityIqTelemetry(args) {
16638
16704
  const logger = args.logger ?? nullLogger;
16639
16705
  if (!args.communityIqEnabled) {
16640
- logger.debug("Community IQ disabled, skipping detection telemetry");
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 detection telemetry: missing installation id");
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("/v2/detection"), {
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("Detection telemetry send failed", {
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("Detection telemetry sent", {
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/engine.js
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 CONFIDENCE_THRESHOLD = 0.85;
16708
- var SENSITIVITY_THRESHOLDS = {
16709
- paranoid: 0.7,
16710
- balanced: 0.85,
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
- threshold;
16760
- sensitivity;
16761
- constructor(sensitivity = "balanced") {
16762
- this.threshold = SENSITIVITY_THRESHOLDS[sensitivity] ?? CONFIDENCE_THRESHOLD;
16763
- this.sensitivity = sensitivity;
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.sort((a, b) => (DECISION_PRIORITY[b.decision] ?? 0) - (DECISION_PRIORITY[a.decision] ?? 0));
16771
- const top = signals[0];
16772
- if (!top) {
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
- decision: ACTION_MAP[match.threat.action] ?? "ask",
16798
- category: match.threat.category,
16799
- confidence: match.threat.confidence,
16800
- severity: SEVERITY_MAP[match.threat.severity] ?? "warning",
16885
+ category,
16886
+ confidence,
16887
+ severity,
16801
16888
  source: "heuristic",
16802
- threatId: match.threat.id,
16803
- reason: match.threat.title,
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: 1,
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: result.risk,
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 (!suppressMediumPi && result.risk >= mediumRisk) {
16957
+ } else if (result.risk >= mediumRisk) {
16875
16958
  signals.push({
16876
- decision: "ask",
16877
16959
  category: "prompt_injection",
16878
- confidence: result.risk,
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: 1,
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: 0.6,
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: 1,
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 import_node_crypto7 = require("node:crypto");
17015
+ var import_node_crypto6 = require("node:crypto");
16956
17016
  init_config();
16957
17017
 
16958
17018
  // ../core/dist/exceptions.js
16959
- var import_node_crypto6 = require("node:crypto");
16960
- var import_node_path13 = require("node:path");
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 import_promises5 = require("node:fs/promises");
16966
- var import_node_path12 = require("node:path");
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(allowlistsDir, logger = nullLogger) {
17030
+ async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
16971
17031
  let files;
16972
17032
  try {
16973
- files = (await (0, import_promises5.readdir)(allowlistsDir)).filter((f) => f.endsWith(".yaml")).sort();
17033
+ files = (await (0, import_promises6.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
16974
17034
  } catch {
16975
- logger.debug("Allowlists directory does not exist", { path: allowlistsDir });
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, import_node_path12.join)(allowlistsDir, filename);
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 ${allowlistsDir}`);
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, import_node_crypto6.createHash)("sha256").update(`${decision}:${match}:${pattern}`).digest("hex");
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, import_node_path13.normalize)(expanded);
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 + import_node_path13.sep))
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.9,
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.75,
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 import_node_path14 = require("node:path");
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, import_node_path14.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
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: (/* @__PURE__ */ new Date()).toISOString()
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 import_promises6 = require("node:fs/promises");
18090
- var import_node_path15 = require("node:path");
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, import_promises6.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
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, import_node_path15.join)(threatDir, filename);
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 flags = record.case_insensitive === true ? "i" : "";
18168
- compiledPattern = new RegExp(record.pattern, flags);
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: Number(record.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: 1,
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 loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
18247
- const eventId = (0, import_node_crypto7.randomUUID)();
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/allowlist checks failed open", { error: String(error) });
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.allowlistsDir, logger);
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
- amsiClient = new AmsiClient(logger);
18392
- await amsiClient.init();
18393
- if (amsiClient.isAvailable) {
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 amsiClient.scanString(scan.scanType, scan.name, scan.content);
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
- amsiClient?.close();
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 piDenySignals = [];
18437
- const heuristicPiDeny = heuristicMatches.some((m) => m.threat.action === "block" && m.threat.category === "prompt_injection");
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
- const engine = new DecisionEngine(config2.sensitivity);
18469
- let verdict = await engine.decide({
18470
- heuristicMatches,
18471
- urlCheckResults,
18472
- packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
18473
- amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
18474
- piCheckResults: piDenySignals.length > 0 ? piDenySignals : void 0,
18475
- piThresholds: {
18476
- highRisk: config2.pi_check.high_risk_threshold,
18477
- mediumRisk: config2.pi_check.medium_risk_threshold
18478
- }
18479
- });
18480
- if (cachedUrlVerdicts.size > 0 && verdict.decision === "allow") {
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
- decision: cachedVerdict.verdict,
18487
- category: "network_egress",
18488
- confidence: 1,
18489
- severity: cachedVerdict.severity,
18490
- source: `cache(${cachedVerdict.source})`,
18491
- artifacts: [url],
18492
- matchedThreatId: null,
18493
- reasons: cachedVerdict.reasons
18494
- };
18495
- break;
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 = config2.pi_check.medium_risk_threshold;
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
- if (verdict.decision === "deny") {
18673
+ const submitTelemetry = async (blocking) => {
18597
18674
  try {
18598
- await sendCommunityIqDetection({
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("Detection telemetry failed open", { error: String(error) });
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 >= config2.pi_check.medium_risk_threshold && r.risk < config2.pi_check.high_risk_threshold);
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 && cached.verdict !== "allow") {
18698
- results.push({
18699
- packageName: pkg.name,
18700
- registry: pkg.registry,
18701
- verdict: cached.verdict === "deny" ? "malicious" : "suspicious_age",
18702
- confidence: 1,
18703
- details: cached.reasons.join("; ")
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: config2.pi_check.medium_risk_threshold,
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" || s === "high")
18859
+ if (s === "critical")
18754
18860
  return "\u{1F6A8}";
18755
- if (s === "medium" || s === "warn" || s === "warning")
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 totalHighCrit(results) {
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 === "critical" || f.severity === "high")
18886
+ if (f.severity !== "info")
18781
18887
  n++;
18782
18888
  }
18783
18889
  }
18784
18890
  return n;
18785
18891
  }
18786
- function findFirstHighCrit(results) {
18892
+ function findFirstNonInfo(results) {
18787
18893
  for (const r of results) {
18788
18894
  for (const f of r.findings) {
18789
- if (f.severity === "critical" || f.severity === "high") {
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 highCrit = result.findings.filter((f) => f.severity === "critical" || f.severity === "high");
18803
- if (highCrit.length === 0)
18908
+ const nonInfo = result.findings.filter((f) => f.severity !== "info");
18909
+ if (nonInfo.length === 0)
18804
18910
  continue;
18805
- for (const f of highCrit.slice(0, MAX_FINDINGS)) {
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 = highCrit.length - MAX_FINDINGS;
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 = totalHighCrit(results);
18840
- const first = findFirstHighCrit(results);
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
- return artifacts.slice(0, 3).map((artifact) => `${artifact.type} '${truncateValue(artifact.value)}'`).join(", ");
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 reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 5).join("; ") : verdict.category;
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/marketplace-migration.js
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((resolve9) => {
18992
- timeout = setTimeout(() => resolve9("timeout"), remainingMs);
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 import_node_crypto8 = require("node:crypto");
19041
- var import_promises7 = require("node:fs/promises");
19042
- var import_node_path16 = require("node:path");
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, import_node_path16.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
19046
- var CACHE_TTL_DAYS = 7;
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, import_node_crypto8.createHash)("sha256");
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, import_promises7.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
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, import_node_path16.join)(dir, file));
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 < CACHE_TTL_DAYS * 24 * 60 * 60 * 1e3;
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 import_node_crypto10 = require("node:crypto");
19154
- var import_promises9 = require("node:fs/promises");
19155
- var import_node_path18 = require("node:path");
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 import_node_crypto9 = require("node:crypto");
19160
- var import_promises8 = require("node:fs/promises");
19161
- var import_node_path17 = require("node:path");
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}${import_node_path17.sep}`);
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, import_node_path17.resolve)(dirPath);
19176
- const rootReal = await (0, import_promises8.realpath)(absDir);
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, import_promises8.realpath)(currentPath);
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, import_promises8.readdir)(currentPath);
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, import_node_path17.join)(currentPath, item);
19191
- const st = await (0, import_promises8.stat)(fullPath);
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, import_node_path17.join)(currentPath, d);
19201
- const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
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, import_node_path17.join)(currentPath, f);
19207
- const fileReal = await (0, import_promises8.realpath)(fullPath);
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
- const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
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, import_promises8.readFile)(fullPath)
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, import_node_crypto9.createHash)("sha256").update(e.isDir ? Buffer.alloc(0) : e.content).digest("hex");
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
- treeEntries.sort((a, b) => a.entryPath < b.entryPath ? -1 : a.entryPath > b.entryPath ? 1 : 0);
19244
- const skillHasher = (0, import_node_crypto9.createHash)("sha256");
19245
- for (const e of treeEntries) {
19246
- skillHasher.update(Buffer.from(`${e.type}\0${e.entryPath}\0${e.hash}
19247
- `, "utf8"));
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 { skillId: skillHasher.digest("hex"), fileHashes };
19834
+ return merged.length > 0 ? merged : void 0;
19250
19835
  }
19251
- async function findSkillPackages(rootDir) {
19252
- const found = [];
19253
- let rootReal;
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
- const rootStat = await (0, import_promises8.stat)(rootDir);
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 found;
19844
+ return { entries: {} };
19261
19845
  }
19262
- const visited = /* @__PURE__ */ new Set();
19263
- async function walk(dir) {
19264
- let real;
19265
- try {
19266
- real = await (0, import_promises8.realpath)(dir);
19267
- } catch {
19268
- return;
19269
- }
19270
- if (visited.has(real))
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 fullPath = (0, import_node_path17.join)(dir, item);
19294
- let st;
19295
- try {
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
- if (st.isDirectory()) {
19301
- await walk(fullPath);
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 computeSkillIdsForRoot(rootDir) {
19309
- const folders = await findSkillPackages(rootDir);
19310
- const out = [];
19311
- for (const folder of folders) {
19312
- try {
19313
- const entries = await entriesFromDirectory(folder);
19314
- const { skillId } = computeSkillId(entries);
19315
- out.push({ folder, skillId });
19316
- } catch {
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
- return out;
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, import_promises9.stat)(dirOrFile);
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, import_node_path18.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
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, import_promises9.readdir)(dirOrFile);
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, import_node_path18.join)(dirOrFile, entry);
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) : Promise.resolve();
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, import_node_path18.relative)(plugin.installPath, filePath)}`;
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, import_node_path18.relative)(plugin.installPath, filePath)
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, import_node_crypto10.createHash)("sha256").update(rawBytes).digest("hex");
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, import_node_path18.relative)(plugin.installPath, filePath)
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([urlCheckPromise, fileCheckPromise, skillCheckPromise]);
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
- async function runSkillCheck(plugin, findings, logger) {
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 ids = skills.map((s) => s.skillId);
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(ids);
19490
- for (const { folder, skillId } of skills) {
19491
- const verdict = verdicts.get(skillId);
19492
- if (!verdict)
19493
- continue;
19494
- const risk = (verdict.overallRiskLevel ?? "").toUpperCase();
19495
- if (risk !== "HIGH" && risk !== "CRITICAL")
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
- const severity = risk === "CRITICAL" ? "critical" : "high";
19498
- findings.push({
19499
- threatId: "SKILL_CHECK",
19500
- title: verdict.summary?.trim() || `Risky skill detected (${risk})`,
19501
- severity,
19502
- confidence: 1,
19503
- action: "log",
19504
- artifact: skillId.slice(0, 16),
19505
- sourceFile: (0, import_node_path18.relative)(plugin.installPath, folder) || ".",
19506
- recommendations: verdict.recommendations.length > 0 ? verdict.recommendations : void 0
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
- const { action: _, ...rest } = toFindingData(finding);
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.allowlistsDir);
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
- storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
19657
- cacheModified = true;
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
- allowlistsDir: ctx.allowlistsDir,
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
- return { scanResults, versionCheck };
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, allowlistsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose") {
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
- allowlistsDir
20680
+ trustedDomainsDir
19875
20681
  });
19876
20682
  const result = await runSessionStart({
19877
20683
  plugins,
19878
20684
  threatsDir,
19879
- allowlistsDir,
20685
+ trustedDomainsDir,
19880
20686
  version,
19881
20687
  logger,
19882
20688
  agentRuntime,
19883
- modelDownloadWorkerPath
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, allowlistsDir, version, agentRuntime, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
20723
+ const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, onNotices, modelDownloadWorkerPath, skillUploadWorkerPath, style = "verbose", commitUploadNotice, announceCleanScans = true } = options;
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, allowlistsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style);
19910
- onResult?.(msg);
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 import_node_path19 = require("node:path");
19949
- var packageRoot = (0, import_node_path19.resolve)(__dirname, "..");
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, import_node_path19.join)(packageRoot, "resources", "threats"),
19953
- allowlistsDir: (0, import_node_path19.join)(packageRoot, "resources", "allowlists")
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, import_node_path19.join)(packageRoot, "package.json"), "utf-8"));
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 import_node_path21 = require("node:path");
20793
+ var import_node_path28 = require("node:path");
19970
20794
 
19971
20795
  // src/plugin-discovery.ts
19972
- var import_promises10 = require("node:fs/promises");
20796
+ var import_promises12 = require("node:fs/promises");
19973
20797
  var import_node_os4 = require("node:os");
19974
- var import_node_path20 = require("node:path");
19975
- var DEFAULT_EXTENSIONS_DIR = (0, import_node_path20.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
19976
- async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding) {
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, import_promises10.readdir)(extensionsDir);
20811
+ entries = await (0, import_promises12.readdir)(extensionsDir);
19983
20812
  } catch {
19984
20813
  logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
19985
- return [];
20814
+ entries = [];
19986
20815
  }
19987
- const plugins = [];
19988
20816
  for (const entry of entries) {
19989
- const extDir = (0, import_node_path20.join)(extensionsDir, entry);
20817
+ const extDir = (0, import_node_path27.join)(extensionsDir, entry);
19990
20818
  let stats;
19991
20819
  try {
19992
- stats = await (0, import_promises10.stat)(extDir);
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, import_node_path20.join)(extDir, "package.json");
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, allowlistsDir } = getBundledDataDirs();
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
- allowlistsDir,
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
- modelDownloadWorkerPath: (0, import_node_path21.resolve)(__dirname, "model-download-worker.cjs")
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(getFindings, clearFindings, logger, branding2 = defaultBranding) {
20049
- return () => {
20050
- const findings = getFindings();
20051
- if (!findings) return void 0;
20052
- clearFindings();
20053
- logger.debug(`${branding2.name}: surfacing plugin scan findings via before_agent_start`);
20054
- const prependContext = [
20055
- '<security-alert source="sage-plugin-scan">',
20056
- findings,
20057
- "",
20058
- "Inform the user about these security findings.",
20059
- "</security-alert>"
20060
- ].join("\n");
20061
- return { prependContext };
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, allowlistsDir, branding2 = defaultBranding) {
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, allowlistsDir, logger },
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 reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 3).join("; ") : verdict.category;
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, allowlistsDir } = getBundledDataDirs();
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 getPendingFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
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, allowlistsDir, branding),
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
- "before_agent_start",
20273
- createBeforeAgentStartHandler(
20274
- getPendingFindings,
20275
- () => {
20276
- pendingConfigurationWarnings = null;
20277
- pendingScanFindings = null;
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