@gendigital/sage-openclaw 0.10.0 → 0.11.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 (35) hide show
  1. package/README.md +4 -4
  2. package/dist/index.cjs +396 -468
  3. package/dist/index.cjs.map +4 -4
  4. package/dist/mcp-server.cjs +31776 -0
  5. package/dist/mcp-server.cjs.map +7 -0
  6. package/dist/model-download-worker.cjs +5 -19
  7. package/dist/model-download-worker.cjs.map +2 -2
  8. package/openclaw.plugin.json +1 -1
  9. package/package.json +4 -3
  10. package/resources/skills/security-awareness/SKILL.md +5 -1
  11. package/resources/threats/agent-layer.yaml +186 -0
  12. package/resources/threats/commands.yaml +24 -53
  13. package/resources/threats/credentials.yaml +12 -20
  14. package/resources/threats/dummy.yaml +22 -33
  15. package/resources/threats/files.yaml +34 -53
  16. package/resources/threats/mac-commands.yaml +20 -47
  17. package/resources/threats/mac-credentials.yaml +4 -12
  18. package/resources/threats/mac-files.yaml +5 -15
  19. package/resources/threats/mac-obfuscation.yaml +6 -13
  20. package/resources/threats/mac-persistence.yaml +11 -20
  21. package/resources/threats/mac-supply-chain.yaml +5 -8
  22. package/resources/threats/mitre.yaml +105 -167
  23. package/resources/threats/obfuscation.yaml +7 -15
  24. package/resources/threats/persistence.yaml +10 -17
  25. package/resources/threats/prompt-injection.yaml +92 -43
  26. package/resources/threats/self-defense.yaml +8 -12
  27. package/resources/threats/supply_chain.yaml +1 -2
  28. package/resources/threats/urls.yaml +9 -14
  29. package/resources/threats/win-commands.yaml +46 -106
  30. package/resources/threats/win-credentials.yaml +14 -24
  31. package/resources/threats/win-files.yaml +10 -20
  32. package/resources/threats/win-obfuscation.yaml +21 -37
  33. package/resources/threats/win-persistence.yaml +8 -16
  34. package/resources/threats/win-supply-chain.yaml +2 -3
  35. /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((resolve9) => setTimeout(resolve9, 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);
@@ -4233,7 +4251,7 @@ var init_zod = __esm({
4233
4251
  });
4234
4252
 
4235
4253
  // ../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;
4254
+ var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4237
4255
  var init_types2 = __esm({
4238
4256
  "../core/dist/types.js"() {
4239
4257
  "use strict";
@@ -4254,23 +4272,21 @@ var init_types2 = __esm({
4254
4272
  value: external_exports.string(),
4255
4273
  context: external_exports.string().optional()
4256
4274
  });
4257
- SeveritySchema = external_exports.enum(["critical", "high", "medium", "low"]);
4258
- ActionSchema = external_exports.enum(["block", "require_approval", "log"]);
4275
+ VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
4259
4276
  ThreatSchema = external_exports.object({
4260
4277
  id: external_exports.string(),
4261
4278
  version: external_exports.number().int().optional(),
4262
4279
  category: external_exports.string(),
4263
- severity: SeveritySchema,
4280
+ severity: VerdictSeveritySchema,
4264
4281
  confidence: external_exports.number(),
4265
- action: ActionSchema,
4266
4282
  pattern: external_exports.string(),
4267
4283
  match_on: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]),
4268
4284
  title: external_exports.string(),
4269
4285
  expires_at: external_exports.string().nullable().optional(),
4270
- revoked: external_exports.boolean().optional().default(false)
4286
+ revoked: external_exports.boolean().optional().default(false),
4287
+ flags: external_exports.array(external_exports.string()).optional().default([])
4271
4288
  });
4272
4289
  DecisionSchema = external_exports.enum(["allow", "deny", "ask"]);
4273
- VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
4274
4290
  SensitivitySchema = external_exports.enum(["paranoid", "balanced", "relaxed"]);
4275
4291
  UrlCheckConfigSchema = external_exports.object({
4276
4292
  endpoint: external_exports.string().optional(),
@@ -4283,9 +4299,6 @@ var init_types2 = __esm({
4283
4299
  ttl_clean_seconds: external_exports.number().default(86400),
4284
4300
  path: external_exports.string().default("~/.sage/cache.json")
4285
4301
  });
4286
- AllowlistConfigSchema = external_exports.object({
4287
- path: external_exports.string().default("~/.sage/allowlist.json")
4288
- });
4289
4302
  LoggingConfigSchema = external_exports.object({
4290
4303
  enabled: external_exports.boolean().default(true),
4291
4304
  log_clean: external_exports.boolean().default(false),
@@ -4347,7 +4360,6 @@ var init_types2 = __esm({
4347
4360
  pi_check: PiCheckConfigSchema.default({}),
4348
4361
  heuristics_enabled: external_exports.boolean().default(true),
4349
4362
  cache: CacheConfigSchema.default({}),
4350
- allowlist: AllowlistConfigSchema.default({}),
4351
4363
  exceptions: ExceptionsConfigSchema.default({}),
4352
4364
  logging: LoggingConfigSchema.default({}),
4353
4365
  operational_logging: OperationalLoggingConfigSchema.default({}),
@@ -4373,6 +4385,7 @@ __export(config_exports, {
4373
4385
  HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
4374
4386
  SAGE_DIR: () => SAGE_DIR,
4375
4387
  defaultConfigPath: () => defaultConfigPath,
4388
+ getClaudeConfigDir: () => getClaudeConfigDir,
4376
4389
  loadConfig: () => loadConfig,
4377
4390
  loadConfigSync: () => loadConfigSync,
4378
4391
  resolvePath: () => resolvePath
@@ -4386,9 +4399,6 @@ function defaultConfigPath() {
4386
4399
  function defaultCachePath() {
4387
4400
  return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
4388
4401
  }
4389
- function defaultAllowlistPath() {
4390
- return (0, import_node_path2.join)(resolvedSageDir(), "allowlist.json");
4391
- }
4392
4402
  function defaultExceptionsPath() {
4393
4403
  return (0, import_node_path2.join)(resolvedSageDir(), "exceptions.json");
4394
4404
  }
@@ -4405,6 +4415,12 @@ function resolvePath(pathStr) {
4405
4415
  }
4406
4416
  return pathStr;
4407
4417
  }
4418
+ function getClaudeConfigDir() {
4419
+ const envDir = process.env.CLAUDE_CONFIG_DIR;
4420
+ if (envDir)
4421
+ return resolvePath(envDir);
4422
+ return resolvePath("~/.claude");
4423
+ }
4408
4424
  function isWithinDirectory(baseDir, targetPath) {
4409
4425
  const rel = (0, import_node_path2.relative)(baseDir, targetPath);
4410
4426
  if (rel === "")
@@ -4454,7 +4470,6 @@ function sanitizeBrandKey(data, logger) {
4454
4470
  }
4455
4471
  function sanitizeConfigPaths(config2, logger) {
4456
4472
  const cachePath = defaultCachePath();
4457
- const allowlistPath = defaultAllowlistPath();
4458
4473
  const exceptionsPath = defaultExceptionsPath();
4459
4474
  const auditPath = defaultAuditPath();
4460
4475
  const operationalLogPath = defaultOperationalLogPath();
@@ -4464,10 +4479,6 @@ function sanitizeConfigPaths(config2, logger) {
4464
4479
  ...config2.cache,
4465
4480
  path: normalizeStateFilePath(config2.cache.path, cachePath, "cache", logger)
4466
4481
  },
4467
- allowlist: {
4468
- ...config2.allowlist,
4469
- path: normalizeStateFilePath(config2.allowlist.path, allowlistPath, "allowlist", logger)
4470
- },
4471
4482
  exceptions: {
4472
4483
  ...config2.exceptions,
4473
4484
  path: normalizeStateFilePath(config2.exceptions.path, exceptionsPath, "exceptions", logger)
@@ -14115,124 +14126,66 @@ __export(index_exports, {
14115
14126
  });
14116
14127
  module.exports = __toCommonJS(index_exports);
14117
14128
 
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");
14129
+ // ../core/dist/allowlist-migration.js
14125
14130
  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();
14131
+
14132
+ // ../core/dist/brands.js
14133
+ var defaultBranding = { name: "Sage", short_name: "Sage" };
14134
+ var BRANDS = {
14135
+ norton: { name: "Norton AI Agent Protection", short_name: "Norton" }
14136
+ };
14137
+ function resolveBranding(brandKey, logger) {
14138
+ if (!brandKey)
14139
+ return defaultBranding;
14140
+ const entry = BRANDS[brandKey];
14141
+ if (!entry) {
14142
+ logger?.warn(`Unknown brand_key "${brandKey}" in config \u2014 using default branding`);
14143
+ return defaultBranding;
14135
14144
  }
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);
14145
+ return { ...entry, brand_key: brandKey };
14144
14146
  }
14145
14147
 
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;
14148
+ // ../core/dist/allowlist-migration.js
14149
+ init_config();
14150
+ init_file_utils();
14151
+ function defaultAllowlistPath() {
14152
+ return (0, import_node_path3.join)(resolvePath("~/.sage"), "allowlist.json");
14165
14153
  }
14166
- async function loadAllowlist(config2, logger = nullLogger) {
14167
- const path = resolvePath(config2.path);
14168
- let raw;
14154
+ async function checkAllowlistMigration() {
14169
14155
  try {
14170
- raw = await getFileContent(path);
14156
+ const raw = await getFileContent(defaultAllowlistPath());
14157
+ const data = JSON.parse(raw);
14158
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
14159
+ return { needed: false, entryTypes: [] };
14160
+ }
14161
+ const record = data;
14162
+ const found = ["urls", "commands", "file_paths"].filter((key) => typeof record[key] === "object" && record[key] !== null && Object.keys(record[key]).length > 0);
14163
+ return { needed: found.length > 0, entryTypes: found };
14171
14164
  } 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();
14180
- }
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;
14165
+ return { needed: false, entryTypes: [] };
14195
14166
  }
14196
- return {
14197
- urls,
14198
- commands: parseEntries(record.commands ?? {}),
14199
- filePaths
14200
- };
14201
14167
  }
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;
14168
+ function formatAllowlistMigrationWarning(entryTypes, branding2 = defaultBranding) {
14169
+ const typeList = entryTypes.join(", ");
14170
+ const lines = [
14171
+ `${branding2.name}: legacy allowlist.json detected with ${typeList} entries.`,
14172
+ " - URL and file_path entries can be migrated manually to ~/.sage/exceptions.json",
14173
+ " - Command entries are stored as SHA-256 hashes and cannot be recovered",
14174
+ " - See https://github.com/gendigitalinc/sage for migration instructions"
14175
+ ];
14176
+ return lines.join("\n");
14224
14177
  }
14225
14178
 
14226
14179
  // ../core/dist/approval-store.js
14227
- var import_node_crypto3 = require("node:crypto");
14180
+ var import_node_crypto2 = require("node:crypto");
14228
14181
  var PENDING_STALE_MS = 60 * 60 * 1e3;
14229
14182
  var APPROVED_TTL_MS = 10 * 60 * 1e3;
14230
- var ApprovalStore = class _ApprovalStore {
14183
+ var ApprovalStore = class {
14231
14184
  pending = /* @__PURE__ */ new Map();
14232
14185
  approved = /* @__PURE__ */ new Map();
14233
14186
  static actionId(toolName, params, sessionId) {
14234
14187
  const payload = JSON.stringify({ toolName, params, sessionId });
14235
- return (0, import_node_crypto3.createHash)("sha256").update(payload).digest("hex").slice(0, 32);
14188
+ return (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex").slice(0, 32);
14236
14189
  }
14237
14190
  static artifactId(type, value) {
14238
14191
  return `${type}:${value}`;
@@ -14269,40 +14222,6 @@ var ApprovalStore = class _ApprovalStore {
14269
14222
  }
14270
14223
  return true;
14271
14224
  }
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
14225
  cleanup(now = Date.now()) {
14307
14226
  for (const [actionId, entry] of this.pending.entries()) {
14308
14227
  if (now - entry.createdAt >= PENDING_STALE_MS) {
@@ -14318,7 +14237,7 @@ var ApprovalStore = class _ApprovalStore {
14318
14237
  };
14319
14238
 
14320
14239
  // ../core/dist/audit-log.js
14321
- var import_node_crypto4 = require("node:crypto");
14240
+ var import_node_crypto3 = require("node:crypto");
14322
14241
  init_config();
14323
14242
  init_file_utils();
14324
14243
 
@@ -14446,7 +14365,7 @@ async function logVerdict(config2, input) {
14446
14365
  return;
14447
14366
  const entry = {
14448
14367
  type: "runtime_verdict",
14449
- entry_id: input.eventId ?? (0, import_node_crypto4.randomUUID)(),
14368
+ entry_id: input.eventId ?? (0, import_node_crypto3.randomUUID)(),
14450
14369
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14451
14370
  session_id: input.sessionId,
14452
14371
  conversation_id: input.conversationId ?? input.sessionId,
@@ -14488,31 +14407,29 @@ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
14488
14407
  }
14489
14408
  }
14490
14409
 
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;
14503
- }
14504
- return { ...entry, brand_key: brandKey };
14505
- }
14506
-
14507
14410
  // ../core/dist/cache.js
14508
14411
  init_config();
14509
14412
  init_file_utils();
14510
14413
  init_types2();
14414
+
14415
+ // ../core/dist/url-utils.js
14416
+ init_file_utils();
14417
+ function normalizeUrl(raw) {
14418
+ try {
14419
+ const u = new URL(raw);
14420
+ u.hash = "";
14421
+ u.searchParams.sort();
14422
+ return u.toString();
14423
+ } catch {
14424
+ return raw.toLowerCase();
14425
+ }
14426
+ }
14427
+
14428
+ // ../core/dist/cache.js
14511
14429
  var ONE_HOUR = 3600;
14512
14430
  var TWENTY_FOUR_HOURS = 86400;
14513
- var FAR_FUTURE = "9999-12-31T23:59:59+00:00";
14514
14431
  var VerdictCache = class {
14515
- store = { urls: {}, commands: {}, packages: {} };
14432
+ store = { urls: {}, packages: {} };
14516
14433
  path;
14517
14434
  config;
14518
14435
  logger;
@@ -14531,11 +14448,10 @@ var VerdictCache = class {
14531
14448
  const data = JSON.parse(raw);
14532
14449
  this.store = {
14533
14450
  urls: data.urls ?? {},
14534
- commands: data.commands ?? {},
14535
14451
  packages: data.packages ?? {}
14536
14452
  };
14537
14453
  } catch {
14538
- this.store = { urls: {}, commands: {}, packages: {} };
14454
+ this.store = { urls: {}, packages: {} };
14539
14455
  }
14540
14456
  }
14541
14457
  getUrl(url) {
@@ -14567,31 +14483,6 @@ var VerdictCache = class {
14567
14483
  sageVersion: this.version
14568
14484
  };
14569
14485
  }
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
14486
  getPackage(key) {
14596
14487
  if (!this.config.enabled)
14597
14488
  return null;
@@ -14602,7 +14493,9 @@ var VerdictCache = class {
14602
14493
  verdict: entry.verdict,
14603
14494
  severity: entry.severity,
14604
14495
  reasons: entry.reasons,
14605
- source: entry.source
14496
+ source: entry.source,
14497
+ ...entry.packageVerdict !== void 0 ? { packageVerdict: entry.packageVerdict } : {},
14498
+ ...entry.packageConfidence !== void 0 ? { packageConfidence: entry.packageConfidence } : {}
14606
14499
  };
14607
14500
  }
14608
14501
  putPackage(key, verdict, packageAgeDays) {
@@ -14652,10 +14545,6 @@ var VerdictCache = class {
14652
14545
  if (this.isStale(entry))
14653
14546
  delete this.store.urls[key];
14654
14547
  }
14655
- for (const [key, entry] of Object.entries(this.store.commands)) {
14656
- if (this.isStale(entry))
14657
- delete this.store.commands[key];
14658
- }
14659
14548
  for (const [key, entry] of Object.entries(this.store.packages)) {
14660
14549
  if (this.isStale(entry))
14661
14550
  delete this.store.packages[key];
@@ -14947,6 +14836,7 @@ var PersistentPowershellAmsiBackend = class {
14947
14836
  invalidResultLogged = false;
14948
14837
  stdoutBuffer = "";
14949
14838
  pendingResponse = null;
14839
+ scanQueue = Promise.resolve();
14950
14840
  constructor(logger) {
14951
14841
  this.logger = logger;
14952
14842
  }
@@ -14962,6 +14852,19 @@ var PersistentPowershellAmsiBackend = class {
14962
14852
  this.pendingResponse = { resolve: resolve9, reject, timer };
14963
14853
  });
14964
14854
  }
14855
+ async enqueueScan(operation) {
14856
+ const previous = this.scanQueue;
14857
+ let release;
14858
+ this.scanQueue = new Promise((resolve9) => {
14859
+ release = resolve9;
14860
+ });
14861
+ await previous;
14862
+ try {
14863
+ return await operation();
14864
+ } finally {
14865
+ release();
14866
+ }
14867
+ }
14965
14868
  async init() {
14966
14869
  try {
14967
14870
  this.process = spawn("powershell.exe", [
@@ -15034,29 +14937,35 @@ var PersistentPowershellAmsiBackend = class {
15034
14937
  async scanString(content, contentName) {
15035
14938
  if (!this.available || !this.process)
15036
14939
  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}
14940
+ return this.enqueueScan(async () => {
14941
+ const process2 = this.process;
14942
+ if (!this.available || !process2)
14943
+ return null;
14944
+ const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
14945
+ try {
14946
+ const req = JSON.stringify({ content: truncated, contentName });
14947
+ process2.stdin?.write(`${req}
15041
14948
  `);
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
- });
14949
+ const line = await this.waitForLine(PS_TIMEOUT);
14950
+ const amsiResult = parseInt(line, 10);
14951
+ if (Number.isNaN(amsiResult) || amsiResult < 0) {
14952
+ if (!this.invalidResultLogged) {
14953
+ this.invalidResultLogged = true;
14954
+ this.logger.warn("AMSI: PowerShell scan returned invalid result", {
14955
+ stdout: line.slice(0, 100),
14956
+ contentName
14957
+ });
14958
+ }
14959
+ return null;
15051
14960
  }
14961
+ this.logger.debug("AMSI: PowerShell scan result", { contentName, amsiResult });
14962
+ return interpretAmsiResult(amsiResult, content, contentName);
14963
+ } catch (e) {
14964
+ this.logger.warn("AMSI: PowerShell scan failed", { error: String(e), contentName });
14965
+ this.close();
15052
14966
  return null;
15053
14967
  }
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
- }
14968
+ });
15060
14969
  }
15061
14970
  close() {
15062
14971
  if (this.process) {
@@ -15291,7 +15200,7 @@ init_file_utils();
15291
15200
  var import_meta = {};
15292
15201
  function resolveVersion() {
15293
15202
  if (true)
15294
- return "0.10.0";
15203
+ return "0.11.0";
15295
15204
  try {
15296
15205
  const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15297
15206
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -15937,6 +15846,8 @@ ${err.stack}` : String(err);
15937
15846
  */
15938
15847
  async classifyChunk(text) {
15939
15848
  const tok = this.tokenizer;
15849
+ if (!tok)
15850
+ throw new Error("PI tokenizer is not loaded");
15940
15851
  const encoded = tok.call(text, {
15941
15852
  truncation: true,
15942
15853
  max_length: MAX_TOKENS,
@@ -15969,6 +15880,8 @@ ${err.stack}` : String(err);
15969
15880
  */
15970
15881
  chunkText(text) {
15971
15882
  const tok = this.tokenizer;
15883
+ if (!tok)
15884
+ throw new Error("PI tokenizer is not loaded");
15972
15885
  const encoded = tok.tokenize(text, {
15973
15886
  addSpecialTokens: false,
15974
15887
  returnOffsetMapping: true,
@@ -15976,6 +15889,8 @@ ${err.stack}` : String(err);
15976
15889
  });
15977
15890
  const tokenIds = encoded.input_ids;
15978
15891
  const offsets = encoded.offset_mapping;
15892
+ if (!offsets)
15893
+ return [text];
15979
15894
  if (tokenIds.length <= MAX_TOKENS)
15980
15895
  return [text];
15981
15896
  const chunks = [];
@@ -16585,7 +16500,7 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16585
16500
  }
16586
16501
 
16587
16502
  // ../core/dist/installation-id.js
16588
- var import_node_crypto5 = require("node:crypto");
16503
+ var import_node_crypto4 = require("node:crypto");
16589
16504
  var import_promises4 = require("node:fs/promises");
16590
16505
  var import_node_path11 = require("node:path");
16591
16506
  init_config();
@@ -16603,7 +16518,7 @@ async function getInstallationId(sageDirPath) {
16603
16518
  } catch {
16604
16519
  }
16605
16520
  try {
16606
- const id = (0, import_node_crypto5.randomUUID)();
16521
+ const id = (0, import_node_crypto4.randomUUID)();
16607
16522
  await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
16608
16523
  await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16609
16524
  return id;
@@ -16702,30 +16617,27 @@ async function sendCommunityIqDetection(args) {
16702
16617
  }
16703
16618
  }
16704
16619
 
16705
- // ../core/dist/engine.js
16620
+ // ../core/dist/policy.js
16706
16621
  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
16622
+ var SENSITIVITY_POLICY = {
16623
+ paranoid: { denyThreshold: 0.7, askThreshold: 0.3 },
16624
+ balanced: { denyThreshold: 0.85, askThreshold: 0.5 },
16625
+ relaxed: { denyThreshold: 0.95, askThreshold: 0.7 }
16728
16626
  };
16627
+ function applyPolicy(confidence, denyThreshold, askThreshold, logger = nullLogger) {
16628
+ if (!Number.isFinite(confidence) || confidence <= 0 || confidence > 1) {
16629
+ logger.warn("Invalid confidence; treating as allow", { confidence });
16630
+ return "allow";
16631
+ }
16632
+ if (confidence >= denyThreshold)
16633
+ return "deny";
16634
+ if (confidence >= askThreshold)
16635
+ return "ask";
16636
+ return "allow";
16637
+ }
16638
+
16639
+ // ../core/dist/engine.js
16640
+ init_types2();
16729
16641
  var PI_LABEL_ONLY_SUFFIXES = /* @__PURE__ */ new Set([
16730
16642
  "command",
16731
16643
  "stdout",
@@ -16756,29 +16668,25 @@ function formatPiFinding(result) {
16756
16668
  return `${where} (${score})`;
16757
16669
  }
16758
16670
  var DecisionEngine = class {
16759
- threshold;
16760
- sensitivity;
16761
- constructor(sensitivity = "balanced") {
16762
- this.threshold = SENSITIVITY_THRESHOLDS[sensitivity] ?? CONFIDENCE_THRESHOLD;
16763
- this.sensitivity = sensitivity;
16671
+ denyThreshold;
16672
+ askThreshold;
16673
+ logger;
16674
+ constructor(sensitivity = "balanced", logger = nullLogger) {
16675
+ const policy = SENSITIVITY_POLICY[sensitivity] ?? SENSITIVITY_POLICY.balanced;
16676
+ this.denyThreshold = policy.denyThreshold;
16677
+ this.askThreshold = policy.askThreshold;
16678
+ this.logger = logger;
16764
16679
  }
16765
16680
  async decide(sources) {
16766
16681
  const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds);
16767
16682
  if (signals.length === 0) {
16768
16683
  return this.allowVerdict();
16769
16684
  }
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
- }
16685
+ const top = signals.reduce((best, s) => s.confidence > best.confidence ? s : best);
16686
+ const maxConfidence = top.confidence;
16687
+ const decision = applyPolicy(maxConfidence, this.denyThreshold, this.askThreshold, this.logger);
16775
16688
  const allArtifacts = [...new Map(signals.map((s) => [s.artifact, s.artifact])).values()];
16776
16689
  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
16690
  return {
16783
16691
  decision,
16784
16692
  category: top.category,
@@ -16793,24 +16701,26 @@ var DecisionEngine = class {
16793
16701
  collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults, piCheckResults, piThresholds) {
16794
16702
  const signals = [];
16795
16703
  for (const match of heuristicMatches) {
16704
+ const { confidence, category, severity, id, title } = match.threat;
16705
+ if (applyPolicy(confidence, this.denyThreshold, this.askThreshold, this.logger) === "allow")
16706
+ continue;
16796
16707
  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",
16708
+ category,
16709
+ confidence,
16710
+ severity,
16801
16711
  source: "heuristic",
16802
- threatId: match.threat.id,
16803
- reason: match.threat.title,
16712
+ threatId: id,
16713
+ reason: title,
16804
16714
  artifact: match.artifact
16805
16715
  });
16806
16716
  }
16807
16717
  for (const result of urlCheckResults) {
16808
16718
  if (result.isMalicious) {
16719
+ const confidence = 1;
16809
16720
  const findingDetails = result.findings.map((f) => `${f.severityName}/${f.typeName}`).join(", ");
16810
16721
  signals.push({
16811
- decision: "deny",
16812
16722
  category: "network_egress",
16813
- confidence: 1,
16723
+ confidence,
16814
16724
  severity: "critical",
16815
16725
  source: "url_check",
16816
16726
  threatId: null,
@@ -16832,7 +16742,6 @@ var DecisionEngine = class {
16832
16742
  for (const result of amsiCheckResults) {
16833
16743
  if (result.isDetected) {
16834
16744
  signals.push({
16835
- decision: "deny",
16836
16745
  category: "malware",
16837
16746
  confidence: 1,
16838
16747
  severity: "critical",
@@ -16843,7 +16752,6 @@ var DecisionEngine = class {
16843
16752
  });
16844
16753
  } else if (result.isBlockedByAdmin) {
16845
16754
  signals.push({
16846
- decision: "deny",
16847
16755
  category: "malware",
16848
16756
  confidence: 0.9,
16849
16757
  severity: "critical",
@@ -16858,24 +16766,21 @@ var DecisionEngine = class {
16858
16766
  if (piCheckResults) {
16859
16767
  const highRisk = piThresholds?.highRisk ?? DEFAULT_PI_HIGH_RISK_THRESHOLD;
16860
16768
  const mediumRisk = piThresholds?.mediumRisk ?? DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
16861
- const suppressMediumPi = this.sensitivity === "relaxed";
16862
16769
  for (const result of piCheckResults) {
16863
16770
  if (result.risk >= highRisk) {
16864
16771
  signals.push({
16865
- decision: "deny",
16866
16772
  category: "prompt_injection",
16867
- confidence: result.risk,
16773
+ confidence: 1,
16868
16774
  severity: "critical",
16869
16775
  source: "pi_check",
16870
16776
  threatId: "PROMPT_INJECTION",
16871
16777
  reason: buildPiReason("Prompt injection detected", result),
16872
16778
  artifact: result.contentName
16873
16779
  });
16874
- } else if (!suppressMediumPi && result.risk >= mediumRisk) {
16780
+ } else if (result.risk >= mediumRisk) {
16875
16781
  signals.push({
16876
- decision: "ask",
16877
16782
  category: "prompt_injection",
16878
- confidence: result.risk,
16783
+ confidence: 0.6,
16879
16784
  severity: "warning",
16880
16785
  source: "pi_check",
16881
16786
  threatId: "PROMPT_INJECTION",
@@ -16890,21 +16795,10 @@ var DecisionEngine = class {
16890
16795
  packageVerdictToSignal(pkg) {
16891
16796
  switch (pkg.verdict) {
16892
16797
  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
16798
  case "malicious":
16904
16799
  return {
16905
- decision: "deny",
16906
16800
  category: "supply_chain",
16907
- confidence: 1,
16801
+ confidence: pkg.confidence,
16908
16802
  severity: "critical",
16909
16803
  source: "package_check",
16910
16804
  threatId: null,
@@ -16912,21 +16806,10 @@ var DecisionEngine = class {
16912
16806
  artifact: pkg.packageName
16913
16807
  };
16914
16808
  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
16809
  case "unknown":
16926
16810
  return {
16927
- decision: "ask",
16928
16811
  category: "supply_chain",
16929
- confidence: 0.6,
16812
+ confidence: pkg.confidence,
16930
16813
  severity: "warning",
16931
16814
  source: "package_check",
16932
16815
  threatId: null,
@@ -16941,7 +16824,7 @@ var DecisionEngine = class {
16941
16824
  return {
16942
16825
  decision: "allow",
16943
16826
  category: "none",
16944
- confidence: 1,
16827
+ confidence: 0,
16945
16828
  severity: "info",
16946
16829
  source: "none",
16947
16830
  artifacts: [],
@@ -16952,11 +16835,11 @@ var DecisionEngine = class {
16952
16835
  };
16953
16836
 
16954
16837
  // ../core/dist/evaluator.js
16955
- var import_node_crypto7 = require("node:crypto");
16838
+ var import_node_crypto6 = require("node:crypto");
16956
16839
  init_config();
16957
16840
 
16958
16841
  // ../core/dist/exceptions.js
16959
- var import_node_crypto6 = require("node:crypto");
16842
+ var import_node_crypto5 = require("node:crypto");
16960
16843
  var import_node_path13 = require("node:path");
16961
16844
  init_config();
16962
16845
  init_file_utils();
@@ -16967,17 +16850,17 @@ var import_node_path12 = require("node:path");
16967
16850
  var import_yaml = __toESM(require_dist(), 1);
16968
16851
  init_file_utils();
16969
16852
  init_types2();
16970
- async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
16853
+ async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
16971
16854
  let files;
16972
16855
  try {
16973
- files = (await (0, import_promises5.readdir)(allowlistsDir)).filter((f) => f.endsWith(".yaml")).sort();
16856
+ files = (await (0, import_promises5.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
16974
16857
  } catch {
16975
- logger.debug("Allowlists directory does not exist", { path: allowlistsDir });
16858
+ logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
16976
16859
  return [];
16977
16860
  }
16978
16861
  const domains = [];
16979
16862
  for (const filename of files) {
16980
- const filePath = (0, import_node_path12.join)(allowlistsDir, filename);
16863
+ const filePath = (0, import_node_path12.join)(trustedDomainsDir, filename);
16981
16864
  let content;
16982
16865
  try {
16983
16866
  content = await getFileContent(filePath);
@@ -17015,7 +16898,7 @@ async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
17015
16898
  domains.push({ domain: domain.toLowerCase(), reason });
17016
16899
  }
17017
16900
  }
17018
- logger.debug(`Loaded ${domains.length} trusted domains from ${allowlistsDir}`);
16901
+ logger.debug(`Loaded ${domains.length} trusted domains from ${trustedDomainsDir}`);
17019
16902
  return domains;
17020
16903
  }
17021
16904
  function extractDomain(url) {
@@ -17042,7 +16925,7 @@ init_types2();
17042
16925
  var MAX_RULES_WARNING = 100;
17043
16926
  var REGEX_TIMEOUT_MS = 50;
17044
16927
  function computeRuleId(decision, match, pattern) {
17045
- const hash = (0, import_node_crypto6.createHash)("sha256").update(`${decision}:${match}:${pattern}`).digest("hex");
16928
+ const hash = (0, import_node_crypto5.createHash)("sha256").update(`${decision}:${match}:${pattern}`).digest("hex");
17046
16929
  return hash.slice(0, 8);
17047
16930
  }
17048
16931
  async function loadExceptions(config2, logger = nullLogger) {
@@ -17620,7 +17503,7 @@ var PackageChecker = class {
17620
17503
  packageName: pkg.name,
17621
17504
  registry: pkg.registry,
17622
17505
  verdict: "not_found",
17623
- confidence: 0.9,
17506
+ confidence: 0.95,
17624
17507
  details: `Package '${pkg.name}@${pkg.version}' version not found on ${pkg.registry} (hallucinated or unpublished)`
17625
17508
  };
17626
17509
  }
@@ -17653,7 +17536,7 @@ var PackageChecker = class {
17653
17536
  packageName: pkg.name,
17654
17537
  registry: pkg.registry,
17655
17538
  verdict: "suspicious_age",
17656
- confidence: 0.75,
17539
+ confidence: 0.6,
17657
17540
  details: `Package '${pkg.name}' first published ${Math.floor(ageDays)} days ago (suspicious age)`,
17658
17541
  ageDays
17659
17542
  };
@@ -18096,7 +17979,6 @@ var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
18096
17979
  "category",
18097
17980
  "severity",
18098
17981
  "confidence",
18099
- "action",
18100
17982
  "pattern",
18101
17983
  "match_on",
18102
17984
  "title"
@@ -18164,8 +18046,8 @@ async function loadThreats(threatDir, logger = nullLogger) {
18164
18046
  continue;
18165
18047
  let compiledPattern;
18166
18048
  try {
18167
- const flags = record.case_insensitive === true ? "i" : "";
18168
- compiledPattern = new RegExp(record.pattern, flags);
18049
+ const flags2 = record.case_insensitive === true ? "i" : "";
18050
+ compiledPattern = new RegExp(record.pattern, flags2);
18169
18051
  } catch (e) {
18170
18052
  logger.warn(`Skipping threat ${record.id}: invalid regex pattern`, {
18171
18053
  error: String(e)
@@ -18174,19 +18056,28 @@ async function loadThreats(threatDir, logger = nullLogger) {
18174
18056
  }
18175
18057
  const rawMatchOn = record.match_on;
18176
18058
  const matchOn = new Set(Array.isArray(rawMatchOn) ? rawMatchOn : [rawMatchOn]);
18059
+ const rawFlags = record.flags;
18060
+ const flags = Array.isArray(rawFlags) ? rawFlags : [];
18061
+ const confidence = Number(record.confidence);
18062
+ if (!Number.isFinite(confidence) || confidence <= 0 || confidence > 1) {
18063
+ logger.warn(`Skipping threat ${record.id}: invalid confidence value`, {
18064
+ confidence: record.confidence
18065
+ });
18066
+ continue;
18067
+ }
18177
18068
  threats.push({
18178
18069
  id: record.id,
18179
18070
  version: typeof record.version === "number" ? record.version : void 0,
18180
18071
  category: record.category,
18181
18072
  severity: record.severity,
18182
- confidence: Number(record.confidence),
18183
- action: record.action,
18073
+ confidence,
18184
18074
  pattern: record.pattern,
18185
18075
  compiledPattern,
18186
18076
  matchOn,
18187
18077
  title: record.title,
18188
18078
  expiresAt: parseExpiresAt(record.expires_at),
18189
- revoked: false
18079
+ revoked: false,
18080
+ flags
18190
18081
  });
18191
18082
  }
18192
18083
  }
@@ -18226,7 +18117,7 @@ function allowVerdict(source = "none") {
18226
18117
  return {
18227
18118
  decision: "allow",
18228
18119
  category: "none",
18229
- confidence: 1,
18120
+ confidence: 0,
18230
18121
  severity: "info",
18231
18122
  source,
18232
18123
  artifacts: [],
@@ -18234,6 +18125,11 @@ function allowVerdict(source = "none") {
18234
18125
  reasons: []
18235
18126
  };
18236
18127
  }
18128
+ async function resolveEvaluationConfig(context, logger) {
18129
+ if (context.config)
18130
+ return context.config;
18131
+ return loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
18132
+ }
18237
18133
  function shouldSkipPromptInjectionForLocalMarkdown(request) {
18238
18134
  if (request.toolName !== "Write" && request.toolName !== "Edit")
18239
18135
  return false;
@@ -18243,8 +18139,8 @@ function shouldSkipPromptInjectionForLocalMarkdown(request) {
18243
18139
  }
18244
18140
  async function evaluateToolCall(request, context) {
18245
18141
  const logger = context.logger ?? nullLogger;
18246
- const config2 = await loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
18247
- const eventId = (0, import_node_crypto7.randomUUID)();
18142
+ const config2 = await resolveEvaluationConfig(context, logger);
18143
+ const eventId = (0, import_node_crypto6.randomUUID)();
18248
18144
  logger.debug("Tool call evaluation started", {
18249
18145
  eventId,
18250
18146
  toolUseId: request.toolUseId,
@@ -18305,27 +18201,6 @@ async function evaluateToolCall(request, context) {
18305
18201
  logEvaluationCompleted(verdict2);
18306
18202
  return verdict2;
18307
18203
  }
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
18204
  const allowMatch = findAllowException(exceptions, request.artifacts);
18330
18205
  if (allowMatch) {
18331
18206
  const allowV = allowVerdict("exception");
@@ -18348,7 +18223,7 @@ async function evaluateToolCall(request, context) {
18348
18223
  return allowV;
18349
18224
  }
18350
18225
  } catch (error) {
18351
- logger.debug("Exception/allowlist checks failed open", { error: String(error) });
18226
+ logger.debug("Exception checks failed open", { error: String(error) });
18352
18227
  }
18353
18228
  let cache2 = null;
18354
18229
  try {
@@ -18370,7 +18245,7 @@ async function evaluateToolCall(request, context) {
18370
18245
  if (shouldSkipPromptInjectionForLocalMarkdown(request)) {
18371
18246
  threats = threats.filter((t) => t.category !== "prompt_injection");
18372
18247
  }
18373
- const trustedDomains = await loadTrustedDomains(context.allowlistsDir, logger);
18248
+ const trustedDomains = await loadTrustedDomains(context.trustedDomainsDir, logger);
18374
18249
  const heuristics = new HeuristicsEngine(threats, trustedDomains);
18375
18250
  heuristicMatches = heuristics.match(request.artifacts);
18376
18251
  }
@@ -18387,10 +18262,19 @@ async function evaluateToolCall(request, context) {
18387
18262
  const amsiCheckResults = [];
18388
18263
  if (config2.amsi_check.enabled && isAmsiSupported()) {
18389
18264
  let amsiClient = null;
18265
+ let amsiLease = null;
18266
+ let ownsAmsiClient = false;
18390
18267
  try {
18391
- amsiClient = new AmsiClient(logger);
18392
- await amsiClient.init();
18393
- if (amsiClient.isAvailable) {
18268
+ if (context.acquireAmsiClientLease) {
18269
+ amsiLease = await context.acquireAmsiClientLease(logger);
18270
+ amsiClient = amsiLease?.client ?? null;
18271
+ } else {
18272
+ amsiClient = new AmsiClient(logger);
18273
+ ownsAmsiClient = true;
18274
+ await amsiClient.init();
18275
+ }
18276
+ if (amsiClient?.isAvailable) {
18277
+ const activeAmsiClient = amsiClient;
18394
18278
  const scans = [];
18395
18279
  if (request.toolName === "Bash") {
18396
18280
  const command = request.toolInput.command ?? "";
@@ -18421,7 +18305,7 @@ async function evaluateToolCall(request, context) {
18421
18305
  }
18422
18306
  }
18423
18307
  for (const scan of scans) {
18424
- const result = await amsiClient.scanString(scan.scanType, scan.name, scan.content);
18308
+ const result = await activeAmsiClient.scanString(scan.scanType, scan.name, scan.content);
18425
18309
  if (result) {
18426
18310
  amsiCheckResults.push(result);
18427
18311
  }
@@ -18429,16 +18313,26 @@ async function evaluateToolCall(request, context) {
18429
18313
  }
18430
18314
  } catch {
18431
18315
  } finally {
18432
- amsiClient?.close();
18316
+ try {
18317
+ if (amsiLease) {
18318
+ await amsiLease.release();
18319
+ } else if (ownsAmsiClient) {
18320
+ amsiClient?.close();
18321
+ }
18322
+ } catch {
18323
+ }
18433
18324
  }
18434
18325
  }
18326
+ const engine = new DecisionEngine(config2.sensitivity, logger);
18327
+ const preliminaryVerdict = await engine.decide({
18328
+ heuristicMatches,
18329
+ urlCheckResults,
18330
+ packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
18331
+ amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0
18332
+ });
18435
18333
  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") {
18334
+ const alreadyDenied = preliminaryVerdict.decision === "deny" || cachedUrlVerdicts.size > 0 && [...cachedUrlVerdicts.values()].some((v) => v.verdict === "deny");
18335
+ if (config2.pi_check.enabled && !alreadyDenied && request.toolName === "WebFetch") {
18442
18336
  try {
18443
18337
  const url = extractWebFetchUrl(request.toolInput);
18444
18338
  if (url) {
@@ -18454,9 +18348,6 @@ async function evaluateToolCall(request, context) {
18454
18348
  const result = await provider.checkContent(fetched.content, `WebFetch:${url}`);
18455
18349
  if (result) {
18456
18350
  allPiResults.push(result);
18457
- if (result.risk >= config2.pi_check.high_risk_threshold) {
18458
- piDenySignals.push(result);
18459
- }
18460
18351
  }
18461
18352
  }
18462
18353
  }
@@ -18465,34 +18356,41 @@ async function evaluateToolCall(request, context) {
18465
18356
  } catch {
18466
18357
  }
18467
18358
  }
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") {
18359
+ let verdict;
18360
+ if (allPiResults.length > 0) {
18361
+ verdict = await engine.decide({
18362
+ heuristicMatches,
18363
+ urlCheckResults,
18364
+ packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
18365
+ amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
18366
+ piCheckResults: allPiResults,
18367
+ piThresholds: {
18368
+ highRisk: config2.pi_check.high_risk_threshold,
18369
+ mediumRisk: config2.pi_check.medium_risk_threshold
18370
+ }
18371
+ });
18372
+ } else {
18373
+ verdict = preliminaryVerdict;
18374
+ }
18375
+ if (cachedUrlVerdicts.size > 0 && verdict.decision !== "deny") {
18481
18376
  for (const [url, cachedVerdict] of cachedUrlVerdicts) {
18482
18377
  if (cachedVerdict.verdict === "allow") {
18483
18378
  continue;
18484
18379
  }
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;
18380
+ if (cachedVerdict.verdict === "deny" || verdict.decision === "allow") {
18381
+ verdict = {
18382
+ decision: cachedVerdict.verdict,
18383
+ category: "network_egress",
18384
+ confidence: 1,
18385
+ severity: cachedVerdict.severity,
18386
+ source: `cache(${cachedVerdict.source})`,
18387
+ artifacts: [url],
18388
+ matchedThreatId: null,
18389
+ reasons: cachedVerdict.reasons
18390
+ };
18391
+ if (verdict.decision === "deny")
18392
+ break;
18393
+ }
18496
18394
  }
18497
18395
  }
18498
18396
  await cacheUrlResults(urlCheckResults, cache2);
@@ -18690,21 +18588,41 @@ async function checkPackages(request, config2, cache2, logger) {
18690
18588
  }
18691
18589
  if (!parsedPackages || parsedPackages.length === 0)
18692
18590
  return results;
18591
+ const DENY_CLASS_PKG_VERDICTS = /* @__PURE__ */ new Set(["malicious", "not_found"]);
18592
+ const ASK_CLASS_PKG_VERDICTS = /* @__PURE__ */ new Set(["suspicious_age", "unknown"]);
18593
+ const VALID_PKG_VERDICTS = /* @__PURE__ */ new Set([...DENY_CLASS_PKG_VERDICTS, ...ASK_CLASS_PKG_VERDICTS]);
18693
18594
  const uncached = [];
18694
18595
  for (const pkg of parsedPackages) {
18695
18596
  const cacheKey2 = `${pkg.registry}:${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`;
18696
18597
  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("; ")
18598
+ if (!cached) {
18599
+ uncached.push(pkg);
18600
+ continue;
18601
+ }
18602
+ if (cached.verdict === "allow")
18603
+ continue;
18604
+ const rawVerdict = cached.packageVerdict;
18605
+ const rawConf = cached.packageConfidence;
18606
+ const verdictValid = rawVerdict !== void 0 && VALID_PKG_VERDICTS.has(rawVerdict);
18607
+ const confValid = rawConf !== void 0 && Number.isFinite(rawConf) && rawConf > 0 && rawConf <= 1;
18608
+ const consistent = rawVerdict === void 0 || !verdictValid || cached.verdict === "deny" && DENY_CLASS_PKG_VERDICTS.has(rawVerdict) || cached.verdict === "ask" && ASK_CLASS_PKG_VERDICTS.has(rawVerdict);
18609
+ if (!verdictValid || !confValid || !consistent) {
18610
+ logger.warn("Package cache entry has invalid metadata; re-querying live", {
18611
+ cacheKey: cacheKey2,
18612
+ packageVerdict: rawVerdict,
18613
+ packageConfidence: rawConf,
18614
+ cachedVerdict: cached.verdict
18704
18615
  });
18705
- } else if (!cached) {
18706
18616
  uncached.push(pkg);
18617
+ continue;
18707
18618
  }
18619
+ results.push({
18620
+ packageName: pkg.name,
18621
+ registry: pkg.registry,
18622
+ verdict: rawVerdict,
18623
+ confidence: rawConf,
18624
+ details: cached.reasons.join("; ")
18625
+ });
18708
18626
  }
18709
18627
  if (uncached.length > 0) {
18710
18628
  const checker = new PackageChecker({
@@ -18724,7 +18642,9 @@ async function checkPackages(request, config2, cache2, logger) {
18724
18642
  verdict: result.verdict === "clean" ? "allow" : isCritical ? "deny" : "ask",
18725
18643
  severity: isCritical ? "critical" : "warning",
18726
18644
  reasons: [result.details],
18727
- source: "package_check"
18645
+ source: "package_check",
18646
+ packageVerdict: result.verdict,
18647
+ packageConfidence: result.confidence
18728
18648
  }, result.ageDays ?? null);
18729
18649
  }
18730
18650
  }
@@ -18750,9 +18670,9 @@ var PAD = 12;
18750
18670
  var SEPARATOR_WIDTH = 48;
18751
18671
  function severityEmoji(severity) {
18752
18672
  const s = severity.toLowerCase();
18753
- if (s === "critical" || s === "high")
18673
+ if (s === "critical")
18754
18674
  return "\u{1F6A8}";
18755
- if (s === "medium" || s === "warn" || s === "warning")
18675
+ if (s === "warning")
18756
18676
  return "\u26A0\uFE0F";
18757
18677
  return "\u2139\uFE0F";
18758
18678
  }
@@ -18773,20 +18693,20 @@ ${formatUpdateNotice(versionCheck)}`;
18773
18693
  }
18774
18694
  return base;
18775
18695
  }
18776
- function totalHighCrit(results) {
18696
+ function totalNonInfo(results) {
18777
18697
  let n = 0;
18778
18698
  for (const r of results) {
18779
18699
  for (const f of r.findings) {
18780
- if (f.severity === "critical" || f.severity === "high")
18700
+ if (f.severity !== "info")
18781
18701
  n++;
18782
18702
  }
18783
18703
  }
18784
18704
  return n;
18785
18705
  }
18786
- function findFirstHighCrit(results) {
18706
+ function findFirstNonInfo(results) {
18787
18707
  for (const r of results) {
18788
18708
  for (const f of r.findings) {
18789
- if (f.severity === "critical" || f.severity === "high") {
18709
+ if (f.severity !== "info") {
18790
18710
  return { plugin: r.plugin, finding: f };
18791
18711
  }
18792
18712
  }
@@ -18799,10 +18719,10 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18799
18719
  const MAX_FINDINGS = 5;
18800
18720
  let first = true;
18801
18721
  for (const result of results) {
18802
- const highCrit = result.findings.filter((f) => f.severity === "critical" || f.severity === "high");
18803
- if (highCrit.length === 0)
18722
+ const nonInfo = result.findings.filter((f) => f.severity !== "info");
18723
+ if (nonInfo.length === 0)
18804
18724
  continue;
18805
- for (const f of highCrit.slice(0, MAX_FINDINGS)) {
18725
+ for (const f of nonInfo.slice(0, MAX_FINDINGS)) {
18806
18726
  if (!first)
18807
18727
  lines.push("");
18808
18728
  first = false;
@@ -18814,8 +18734,6 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18814
18734
  lines.push(kv("Artifact", f.artifact));
18815
18735
  }
18816
18736
  lines.push(kv("Source", f.sourceFile));
18817
- const actionLabel = f.action === "block" ? "Blocked" : "Flagged";
18818
- lines.push(kv("Action", actionLabel));
18819
18737
  if (f.recommendations && f.recommendations.length > 0) {
18820
18738
  lines.push(kv("Recommendations", ""));
18821
18739
  for (const rec of f.recommendations) {
@@ -18823,7 +18741,7 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18823
18741
  }
18824
18742
  }
18825
18743
  }
18826
- const overflow = highCrit.length - MAX_FINDINGS;
18744
+ const overflow = nonInfo.length - MAX_FINDINGS;
18827
18745
  if (overflow > 0) {
18828
18746
  lines.push("");
18829
18747
  lines.push(` ... and ${overflow} more findings`);
@@ -18836,8 +18754,8 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18836
18754
  return lines.join("\n");
18837
18755
  }
18838
18756
  function formatThreatBannerCompact(version, results, versionCheck, branding2) {
18839
- const total = totalHighCrit(results);
18840
- const first = findFirstHighCrit(results);
18757
+ const total = totalNonInfo(results);
18758
+ const first = findFirstNonInfo(results);
18841
18759
  if (!first || total === 0) {
18842
18760
  return formatStartupClean(version, versionCheck, branding2);
18843
18761
  }
@@ -18902,21 +18820,23 @@ function truncateValue(value) {
18902
18820
  function summarizeArtifacts(artifacts) {
18903
18821
  if (artifacts.length === 0)
18904
18822
  return "none";
18905
- return artifacts.slice(0, 3).map((artifact) => `${artifact.type} '${truncateValue(artifact.value)}'`).join(", ");
18823
+ const maxArtifacts = 3;
18824
+ const summary = artifacts.slice(0, maxArtifacts).map((artifact) => `${artifact.type} '${truncateValue(artifact.value)}'`).join(", ");
18825
+ const overflow = artifacts.length - maxArtifacts;
18826
+ return overflow > 0 ? `${summary}, ... and ${overflow} more` : summary;
18906
18827
  }
18907
18828
  function formatDenyMessage(verdict, branding2 = defaultBranding) {
18908
- const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 5).join("; ") : verdict.category;
18829
+ const maxReasons = 5;
18830
+ 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
18831
  return [
18910
18832
  `${branding2.name} blocked this action.`,
18911
18833
  `Severity: ${verdict.severity}`,
18912
18834
  `Category: ${verdict.category}`,
18913
- `Reason: ${reasons}`
18835
+ `Reason: ${reasons}`,
18836
+ `If this is a false positive, use the sage_report_false_positive MCP tool to report it.`
18914
18837
  ].join("\n");
18915
18838
  }
18916
18839
 
18917
- // ../core/dist/marketplace-migration.js
18918
- init_config();
18919
-
18920
18840
  // ../core/dist/model-download.js
18921
18841
  init_types2();
18922
18842
 
@@ -19037,18 +18957,19 @@ function createOperationalLogger(config2, runtime) {
19037
18957
  }
19038
18958
 
19039
18959
  // ../core/dist/plugin-scan-cache.js
19040
- var import_node_crypto8 = require("node:crypto");
18960
+ var import_node_crypto7 = require("node:crypto");
19041
18961
  var import_promises7 = require("node:fs/promises");
19042
18962
  var import_node_path16 = require("node:path");
19043
18963
  init_file_utils();
19044
18964
  init_types2();
19045
18965
  var DEFAULT_CACHE_PATH = (0, import_node_path16.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
19046
- var CACHE_TTL_DAYS = 7;
18966
+ var CACHE_TTL_DAYS = 1;
18967
+ var SCHEMA_VERSION = 3;
19047
18968
  function cacheKey(pluginKey, version, lastUpdated) {
19048
18969
  return `${pluginKey}:${version}:${lastUpdated}`;
19049
18970
  }
19050
18971
  async function computeConfigHash(sageVersion, ...dirs) {
19051
- const h = (0, import_node_crypto8.createHash)("sha256");
18972
+ const h = (0, import_node_crypto7.createHash)("sha256");
19052
18973
  if (sageVersion)
19053
18974
  h.update(sageVersion);
19054
18975
  for (const dir of dirs) {
@@ -19077,6 +18998,14 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
19077
18998
  }
19078
18999
  try {
19079
19000
  const data = JSON.parse(raw);
19001
+ const storedVersion = data.schema_version ?? 1;
19002
+ if (storedVersion !== SCHEMA_VERSION) {
19003
+ logger.debug("Schema version changed, invalidating plugin scan cache", {
19004
+ from: storedVersion,
19005
+ to: SCHEMA_VERSION
19006
+ });
19007
+ return { configHash, entries: {} };
19008
+ }
19080
19009
  const storedHash = data.config_hash ?? "";
19081
19010
  if (configHash && storedHash !== configHash) {
19082
19011
  logger.debug("Config hash changed, invalidating plugin scan cache", {
@@ -19104,6 +19033,7 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
19104
19033
  async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nullLogger) {
19105
19034
  try {
19106
19035
  const data = {
19036
+ schema_version: SCHEMA_VERSION,
19107
19037
  config_hash: cache2.configHash,
19108
19038
  entries: Object.fromEntries(Object.entries(cache2.entries).map(([key, entry]) => [
19109
19039
  key,
@@ -19150,13 +19080,14 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
19150
19080
  }
19151
19081
 
19152
19082
  // ../core/dist/plugin-scanner.js
19153
- var import_node_crypto10 = require("node:crypto");
19083
+ var import_node_crypto9 = require("node:crypto");
19154
19084
  var import_promises9 = require("node:fs/promises");
19155
19085
  var import_node_path18 = require("node:path");
19086
+ init_config();
19156
19087
  init_file_utils();
19157
19088
 
19158
19089
  // ../core/dist/skill-id.js
19159
- var import_node_crypto9 = require("node:crypto");
19090
+ var import_node_crypto8 = require("node:crypto");
19160
19091
  var import_promises8 = require("node:fs/promises");
19161
19092
  var import_node_path17 = require("node:path");
19162
19093
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
@@ -19234,14 +19165,14 @@ function computeSkillId(entries) {
19234
19165
  const fileHashes = {};
19235
19166
  const treeEntries = [];
19236
19167
  for (const e of normalized) {
19237
- const contentHash = (0, import_node_crypto9.createHash)("sha256").update(e.isDir ? Buffer.alloc(0) : e.content).digest("hex");
19168
+ const contentHash = (0, import_node_crypto8.createHash)("sha256").update(e.isDir ? Buffer.alloc(0) : e.content).digest("hex");
19238
19169
  const entryType = e.isDir ? "dir" : "file";
19239
19170
  treeEntries.push({ type: entryType, entryPath: e.entryPath, hash: contentHash });
19240
19171
  if (!e.isDir)
19241
19172
  fileHashes[e.entryPath] = contentHash;
19242
19173
  }
19243
19174
  treeEntries.sort((a, b) => a.entryPath < b.entryPath ? -1 : a.entryPath > b.entryPath ? 1 : 0);
19244
- const skillHasher = (0, import_node_crypto9.createHash)("sha256");
19175
+ const skillHasher = (0, import_node_crypto8.createHash)("sha256");
19245
19176
  for (const e of treeEntries) {
19246
19177
  skillHasher.update(Buffer.from(`${e.type}\0${e.entryPath}\0${e.hash}
19247
19178
  `, "utf8"));
@@ -19321,7 +19252,6 @@ async function computeSkillIdsForRoot(rootDir) {
19321
19252
 
19322
19253
  // ../core/dist/plugin-scanner.js
19323
19254
  init_types2();
19324
- var DEFAULT_PLUGINS_REGISTRY = (0, import_node_path18.join)(getHomeDir(), ".claude", "plugins", "installed_plugins.json");
19325
19255
  var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
19326
19256
  ".py",
19327
19257
  ".js",
@@ -19408,8 +19338,6 @@ async function scanPlugin(plugin, options = {}) {
19408
19338
  threatId: "AMSI_SCAN",
19409
19339
  title: `AMSI detection (result=${amsiResult.amsiResult})`,
19410
19340
  severity: "critical",
19411
- confidence: 1,
19412
- action: "block",
19413
19341
  artifact: content.slice(0, 200),
19414
19342
  sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
19415
19343
  });
@@ -19421,7 +19349,7 @@ async function scanPlugin(plugin, options = {}) {
19421
19349
  allUrls.push(...extractUrls(content));
19422
19350
  }
19423
19351
  if (checkFileHashes) {
19424
- const sha256 = (0, import_node_crypto10.createHash)("sha256").update(rawBytes).digest("hex");
19352
+ const sha256 = (0, import_node_crypto9.createHash)("sha256").update(rawBytes).digest("hex");
19425
19353
  const existing = hashToFiles.get(sha256);
19426
19354
  if (existing) {
19427
19355
  existing.push(filePath);
@@ -19442,8 +19370,6 @@ async function scanPlugin(plugin, options = {}) {
19442
19370
  threatId: "URL_CHECK",
19443
19371
  title: `Malicious URL (${findingDetails})`,
19444
19372
  severity: "critical",
19445
- confidence: 1,
19446
- action: "block",
19447
19373
  artifact: ur.url.slice(0, 200),
19448
19374
  sourceFile: "URL check"
19449
19375
  });
@@ -19465,8 +19391,6 @@ async function scanPlugin(plugin, options = {}) {
19465
19391
  threatId: "FILE_CHECK",
19466
19392
  title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
19467
19393
  severity: "critical",
19468
- confidence: 1,
19469
- action: "block",
19470
19394
  artifact: fr.sha256,
19471
19395
  sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
19472
19396
  });
@@ -19494,13 +19418,11 @@ async function runSkillCheck(plugin, findings, logger) {
19494
19418
  const risk = (verdict.overallRiskLevel ?? "").toUpperCase();
19495
19419
  if (risk !== "HIGH" && risk !== "CRITICAL")
19496
19420
  continue;
19497
- const severity = risk === "CRITICAL" ? "critical" : "high";
19421
+ const severity = risk === "CRITICAL" ? "critical" : "warning";
19498
19422
  findings.push({
19499
19423
  threatId: "SKILL_CHECK",
19500
19424
  title: verdict.summary?.trim() || `Risky skill detected (${risk})`,
19501
19425
  severity,
19502
- confidence: 1,
19503
- action: "log",
19504
19426
  artifact: skillId.slice(0, 16),
19505
19427
  sourceFile: (0, import_node_path18.relative)(plugin.installPath, folder) || ".",
19506
19428
  recommendations: verdict.recommendations.length > 0 ? verdict.recommendations : void 0
@@ -19525,8 +19447,6 @@ function fromCachedFinding(finding) {
19525
19447
  threatId: finding.threat_id,
19526
19448
  title: finding.title,
19527
19449
  severity: finding.severity,
19528
- confidence: finding.confidence,
19529
- action: finding.action,
19530
19450
  artifact: finding.artifact,
19531
19451
  sourceFile: finding.source_file,
19532
19452
  recommendations: finding.recommendations
@@ -19537,16 +19457,13 @@ function toFindingData(finding) {
19537
19457
  threat_id: finding.threatId,
19538
19458
  title: finding.title,
19539
19459
  severity: finding.severity,
19540
- confidence: finding.confidence,
19541
- action: finding.action,
19542
19460
  artifact: finding.artifact,
19543
19461
  source_file: finding.sourceFile,
19544
19462
  recommendations: finding.recommendations
19545
19463
  };
19546
19464
  }
19547
19465
  function toAuditFindingData(finding) {
19548
- const { action: _, ...rest } = toFindingData(finding);
19549
- return rest;
19466
+ return { ...toFindingData(finding) };
19550
19467
  }
19551
19468
  async function runSessionStartScan(context) {
19552
19469
  const logger = context.logger ?? nullLogger;
@@ -19591,7 +19508,7 @@ async function runSessionStartScan(context) {
19591
19508
  }
19592
19509
  }
19593
19510
  async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient, logger) {
19594
- const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.allowlistsDir);
19511
+ const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.trustedDomainsDir);
19595
19512
  const cache2 = await loadScanCache(configHash, context.scanCachePath, logger);
19596
19513
  const resultsWithFindings = [];
19597
19514
  let cacheModified = false;
@@ -19617,8 +19534,6 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19617
19534
  threatId: "EXCEPTION-DENY",
19618
19535
  title: `Denied by exception: ${denyMatch.pattern}${denyMatch.reason ? ` \u2014 ${denyMatch.reason}` : ""}`,
19619
19536
  severity: "critical",
19620
- confidence: 1,
19621
- action: "block",
19622
19537
  artifact: plugin.key,
19623
19538
  sourceFile: "~/.sage/exceptions.json"
19624
19539
  }
@@ -19766,7 +19681,7 @@ async function runSessionStart(ctx) {
19766
19681
  runSessionStartScan({
19767
19682
  plugins: ctx.plugins,
19768
19683
  threatsDir: ctx.threatsDir,
19769
- allowlistsDir: ctx.allowlistsDir,
19684
+ trustedDomainsDir: ctx.trustedDomainsDir,
19770
19685
  sageVersion: ctx.version,
19771
19686
  logger,
19772
19687
  configPath: ctx.configPath,
@@ -19866,20 +19781,21 @@ function spawnModelDownloadWorker(args) {
19866
19781
  }
19867
19782
 
19868
19783
  // ../core/dist/scan-handler.js
19869
- async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose") {
19784
+ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion) {
19870
19785
  logger.debug(`${branding2.name} plugin scan started (${context})`, {
19871
19786
  agentRuntime,
19872
19787
  pluginsCount: plugins.length,
19873
19788
  threatsDir,
19874
- allowlistsDir
19789
+ trustedDomainsDir
19875
19790
  });
19876
19791
  const result = await runSessionStart({
19877
19792
  plugins,
19878
19793
  threatsDir,
19879
- allowlistsDir,
19794
+ trustedDomainsDir,
19880
19795
  version,
19881
19796
  logger,
19882
19797
  agentRuntime,
19798
+ agentRuntimeVersion,
19883
19799
  modelDownloadWorkerPath
19884
19800
  });
19885
19801
  const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
@@ -19898,7 +19814,7 @@ async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir
19898
19814
  return formatSessionStartMessage(version, result, branding2, style);
19899
19815
  }
19900
19816
  function createScanHandler(options) {
19901
- const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, allowlistsDir, version, agentRuntime, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
19817
+ const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
19902
19818
  return async () => {
19903
19819
  try {
19904
19820
  let plugins = await discoverPlugins2();
@@ -19906,7 +19822,7 @@ function createScanHandler(options) {
19906
19822
  if (plugins.length === 0) {
19907
19823
  logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
19908
19824
  }
19909
- const msg = await runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style);
19825
+ const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion);
19910
19826
  onResult?.(msg);
19911
19827
  } catch (e) {
19912
19828
  logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
@@ -19950,7 +19866,7 @@ var packageRoot = (0, import_node_path19.resolve)(__dirname, "..");
19950
19866
  function getBundledDataDirs() {
19951
19867
  return {
19952
19868
  threatsDir: (0, import_node_path19.join)(packageRoot, "resources", "threats"),
19953
- allowlistsDir: (0, import_node_path19.join)(packageRoot, "resources", "allowlists")
19869
+ trustedDomainsDir: (0, import_node_path19.join)(packageRoot, "resources", "trusted-domains")
19954
19870
  };
19955
19871
  }
19956
19872
  var cachedVersion;
@@ -20023,7 +19939,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
20023
19939
 
20024
19940
  // src/startup-scan.ts
20025
19941
  function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult) {
20026
- const { threatsDir, allowlistsDir } = getBundledDataDirs();
19942
+ const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
20027
19943
  const version = getSageVersion();
20028
19944
  return createScanHandler({
20029
19945
  logger,
@@ -20031,9 +19947,10 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
20031
19947
  discoverPlugins: () => discoverOpenClawPlugins(logger, void 0, branding2),
20032
19948
  selfPrefix: "@gendigital/sage-openclaw@",
20033
19949
  threatsDir,
20034
- allowlistsDir,
19950
+ trustedDomainsDir,
20035
19951
  version,
20036
19952
  agentRuntime: "openclaw",
19953
+ // No agentRuntimeVersion: the OpenClaw plugin API exposes no host version.
20037
19954
  branding: branding2,
20038
19955
  onResult,
20039
19956
  modelDownloadWorkerPath: (0, import_node_path21.resolve)(__dirname, "model-download-worker.cjs")
@@ -20124,7 +20041,7 @@ function mapToolToArtifacts(toolName, params) {
20124
20041
  return null;
20125
20042
  }
20126
20043
  }
20127
- function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir, branding2 = defaultBranding) {
20044
+ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomainsDir, branding2 = defaultBranding) {
20128
20045
  async function handleToolCall(event, ctx) {
20129
20046
  const sessionId = ctx?.sessionKey ?? "unknown";
20130
20047
  const completeHook = (result, data = {}) => {
@@ -20160,7 +20077,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
20160
20077
  toolInput: normalizeToolInput(toolName, params),
20161
20078
  artifacts
20162
20079
  },
20163
- { threatsDir, allowlistsDir, logger },
20080
+ { threatsDir, trustedDomainsDir, logger },
20164
20081
  approvalStore
20165
20082
  );
20166
20083
  if (verdict.decision === "allow") {
@@ -20183,7 +20100,8 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
20183
20100
  });
20184
20101
  return { block: true, blockReason: formatDenyMessage(verdict, branding2) };
20185
20102
  }
20186
- const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 3).join("; ") : verdict.category;
20103
+ const maxReasons = 3;
20104
+ 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
20105
  const intersected = artifacts.filter((a) => verdict.artifacts.includes(a.value));
20188
20106
  const flaggedArtifacts = intersected.length > 0 ? intersected : artifacts;
20189
20107
  completeHook("evaluated", {
@@ -20251,35 +20169,45 @@ var index_default = {
20251
20169
  const toolLogger = operationalLogger.forComponent("tool-handler");
20252
20170
  const scanLogger = operationalLogger.forComponent("startup-scan");
20253
20171
  const approvalStore = new ApprovalStore();
20254
- const { threatsDir, allowlistsDir } = getBundledDataDirs();
20172
+ const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
20255
20173
  const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
20256
20174
  const interval = setInterval(() => approvalStore.cleanup(), CLEANUP_INTERVAL_MS);
20257
20175
  if (typeof interval.unref === "function") interval.unref();
20258
20176
  let pendingConfigurationWarnings = formatConfigurationWarnings(getConfigurationWarningsSync(void 0, logger), branding) ?? null;
20177
+ const migrationCheckPromise = checkAllowlistMigration().then((result) => {
20178
+ if (result.needed) {
20179
+ const notice = formatAllowlistMigrationWarning(result.entryTypes, branding);
20180
+ pendingConfigurationWarnings = pendingConfigurationWarnings ? `${pendingConfigurationWarnings}
20181
+
20182
+ ${notice}` : notice;
20183
+ }
20184
+ }).catch(() => {
20185
+ });
20259
20186
  let pendingScanFindings = null;
20260
20187
  const onFindings = (msg) => {
20261
20188
  pendingScanFindings = msg;
20262
20189
  };
20263
20190
  const getPendingFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
20191
+ const beforeAgentStartHandler = createBeforeAgentStartHandler(
20192
+ getPendingFindings,
20193
+ () => {
20194
+ pendingConfigurationWarnings = null;
20195
+ pendingScanFindings = null;
20196
+ },
20197
+ logger,
20198
+ branding
20199
+ );
20264
20200
  api.on(
20265
20201
  "before_tool_call",
20266
- createToolCallHandler(approvalStore, toolLogger, threatsDir, allowlistsDir, branding),
20202
+ createToolCallHandler(approvalStore, toolLogger, threatsDir, trustedDomainsDir, branding),
20267
20203
  { priority: 100 }
20268
20204
  );
20269
20205
  api.on("gateway_start", createStartupScanHandler(scanLogger, branding, onFindings));
20270
20206
  api.on("session_start", createSessionScanHandler(scanLogger, branding, onFindings));
20271
- api.on(
20272
- "before_agent_start",
20273
- createBeforeAgentStartHandler(
20274
- getPendingFindings,
20275
- () => {
20276
- pendingConfigurationWarnings = null;
20277
- pendingScanFindings = null;
20278
- },
20279
- logger,
20280
- branding
20281
- )
20282
- );
20207
+ api.on("before_agent_start", async () => {
20208
+ await migrationCheckPromise;
20209
+ return beforeAgentStartHandler();
20210
+ });
20283
20211
  }
20284
20212
  };
20285
20213
  //# sourceMappingURL=index.cjs.map