@gendigital/sage-openclaw 0.9.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 +63 -20
  2. package/dist/index.cjs +1073 -653
  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 +422 -29
  7. package/dist/model-download-worker.cjs.map +4 -4
  8. package/openclaw.plugin.json +1 -1
  9. package/package.json +13 -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 +94 -44
  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 +15 -25
  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, 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),
@@ -4293,6 +4306,14 @@ var init_types2 = __esm({
4293
4306
  max_bytes: external_exports.number().int().min(0).default(5 * 1024 * 1024),
4294
4307
  max_files: external_exports.number().int().min(0).default(3)
4295
4308
  });
4309
+ OperationalLogLevelSchema = external_exports.enum(["debug", "info", "warn", "error"]);
4310
+ OperationalLoggingConfigSchema = external_exports.object({
4311
+ enabled: external_exports.boolean().default(true),
4312
+ level: OperationalLogLevelSchema.default("info"),
4313
+ path: external_exports.string().default("~/.sage/operational.jsonl"),
4314
+ max_bytes: external_exports.number().int().min(0).default(5 * 1024 * 1024),
4315
+ max_files: external_exports.number().int().min(0).default(3)
4316
+ });
4296
4317
  FileCheckConfigSchema = external_exports.object({
4297
4318
  endpoint: external_exports.string().optional(),
4298
4319
  timeout_seconds: external_exports.number().default(5),
@@ -4339,9 +4360,9 @@ var init_types2 = __esm({
4339
4360
  pi_check: PiCheckConfigSchema.default({}),
4340
4361
  heuristics_enabled: external_exports.boolean().default(true),
4341
4362
  cache: CacheConfigSchema.default({}),
4342
- allowlist: AllowlistConfigSchema.default({}),
4343
4363
  exceptions: ExceptionsConfigSchema.default({}),
4344
4364
  logging: LoggingConfigSchema.default({}),
4365
+ operational_logging: OperationalLoggingConfigSchema.default({}),
4345
4366
  sensitivity: SensitivitySchema.default("balanced"),
4346
4367
  disabled_threats: external_exports.array(external_exports.string()).default([]),
4347
4368
  brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
@@ -4363,6 +4384,8 @@ var config_exports = {};
4363
4384
  __export(config_exports, {
4364
4385
  HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
4365
4386
  SAGE_DIR: () => SAGE_DIR,
4387
+ defaultConfigPath: () => defaultConfigPath,
4388
+ getClaudeConfigDir: () => getClaudeConfigDir,
4366
4389
  loadConfig: () => loadConfig,
4367
4390
  loadConfigSync: () => loadConfigSync,
4368
4391
  resolvePath: () => resolvePath
@@ -4376,15 +4399,15 @@ function defaultConfigPath() {
4376
4399
  function defaultCachePath() {
4377
4400
  return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
4378
4401
  }
4379
- function defaultAllowlistPath() {
4380
- return (0, import_node_path2.join)(resolvedSageDir(), "allowlist.json");
4381
- }
4382
4402
  function defaultExceptionsPath() {
4383
4403
  return (0, import_node_path2.join)(resolvedSageDir(), "exceptions.json");
4384
4404
  }
4385
4405
  function defaultAuditPath() {
4386
4406
  return (0, import_node_path2.join)(resolvedSageDir(), "audit.jsonl");
4387
4407
  }
4408
+ function defaultOperationalLogPath() {
4409
+ return (0, import_node_path2.join)(resolvedSageDir(), "operational.jsonl");
4410
+ }
4388
4411
  function resolvePath(pathStr) {
4389
4412
  if (pathStr.startsWith("~/") || pathStr === "~") {
4390
4413
  const home = getHomeDir();
@@ -4392,6 +4415,12 @@ function resolvePath(pathStr) {
4392
4415
  }
4393
4416
  return pathStr;
4394
4417
  }
4418
+ function getClaudeConfigDir() {
4419
+ const envDir = process.env.CLAUDE_CONFIG_DIR;
4420
+ if (envDir)
4421
+ return resolvePath(envDir);
4422
+ return resolvePath("~/.claude");
4423
+ }
4395
4424
  function isWithinDirectory(baseDir, targetPath) {
4396
4425
  const rel = (0, import_node_path2.relative)(baseDir, targetPath);
4397
4426
  if (rel === "")
@@ -4441,19 +4470,15 @@ function sanitizeBrandKey(data, logger) {
4441
4470
  }
4442
4471
  function sanitizeConfigPaths(config2, logger) {
4443
4472
  const cachePath = defaultCachePath();
4444
- const allowlistPath = defaultAllowlistPath();
4445
4473
  const exceptionsPath = defaultExceptionsPath();
4446
4474
  const auditPath = defaultAuditPath();
4475
+ const operationalLogPath = defaultOperationalLogPath();
4447
4476
  return {
4448
4477
  ...config2,
4449
4478
  cache: {
4450
4479
  ...config2.cache,
4451
4480
  path: normalizeStateFilePath(config2.cache.path, cachePath, "cache", logger)
4452
4481
  },
4453
- allowlist: {
4454
- ...config2.allowlist,
4455
- path: normalizeStateFilePath(config2.allowlist.path, allowlistPath, "allowlist", logger)
4456
- },
4457
4482
  exceptions: {
4458
4483
  ...config2.exceptions,
4459
4484
  path: normalizeStateFilePath(config2.exceptions.path, exceptionsPath, "exceptions", logger)
@@ -4461,61 +4486,50 @@ function sanitizeConfigPaths(config2, logger) {
4461
4486
  logging: {
4462
4487
  ...config2.logging,
4463
4488
  path: normalizeStateFilePath(config2.logging.path, auditPath, "logging", logger)
4489
+ },
4490
+ operational_logging: {
4491
+ ...config2.operational_logging,
4492
+ path: normalizeStateFilePath(config2.operational_logging.path, operationalLogPath, "operational_logging", logger)
4464
4493
  }
4465
4494
  };
4466
4495
  }
4467
- async function loadConfig(configPath, logger = nullLogger) {
4468
- const path = configPath ?? defaultConfigPath();
4469
- let raw;
4470
- try {
4471
- raw = await getFileContent(path);
4472
- } catch {
4473
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4474
- }
4496
+ function defaultConfig(logger) {
4497
+ return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4498
+ }
4499
+ function parseConfig(raw, path, logger) {
4475
4500
  let data;
4476
4501
  try {
4477
4502
  data = JSON.parse(raw);
4478
4503
  } catch (e) {
4479
4504
  logger.warn(`Failed to parse config from ${path}`, { error: String(e) });
4480
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4505
+ return defaultConfig(logger);
4481
4506
  }
4482
4507
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
4483
4508
  logger.warn(`Config file ${path} does not contain a JSON object`);
4484
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4509
+ return defaultConfig(logger);
4485
4510
  }
4486
4511
  const sanitized = sanitizeBrandKey(data, logger);
4487
4512
  try {
4488
4513
  return sanitizeConfigPaths(ConfigSchema.parse(sanitized), logger);
4489
4514
  } catch (e) {
4490
4515
  logger.warn(`Config validation failed, using defaults`, { error: String(e) });
4491
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4516
+ return defaultConfig(logger);
4492
4517
  }
4493
4518
  }
4494
- function loadConfigSync(configPath, logger = nullLogger) {
4495
- const path = configPath ?? defaultConfigPath();
4496
- let raw;
4519
+ async function loadConfig(configPath, logger = nullLogger) {
4520
+ const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4497
4521
  try {
4498
- raw = getFileContentSync(path);
4522
+ return parseConfig(await getFileContent(path), path, logger);
4499
4523
  } catch {
4500
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4501
- }
4502
- let data;
4503
- try {
4504
- data = JSON.parse(raw);
4505
- } catch (e) {
4506
- logger.warn(`Failed to parse config from ${path}`, { error: String(e) });
4507
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4508
- }
4509
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
4510
- logger.warn(`Config file ${path} does not contain a JSON object`);
4511
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4524
+ return defaultConfig(logger);
4512
4525
  }
4513
- const sanitized = sanitizeBrandKey(data, logger);
4526
+ }
4527
+ function loadConfigSync(configPath, logger = nullLogger) {
4528
+ const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4514
4529
  try {
4515
- return sanitizeConfigPaths(ConfigSchema.parse(sanitized), logger);
4516
- } catch (e) {
4517
- logger.warn(`Config validation failed, using defaults`, { error: String(e) });
4518
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4530
+ return parseConfig(getFileContentSync(path), path, logger);
4531
+ } catch {
4532
+ return defaultConfig(logger);
4519
4533
  }
4520
4534
  }
4521
4535
  var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, BRAND_KEY_RE;
@@ -14112,124 +14126,66 @@ __export(index_exports, {
14112
14126
  });
14113
14127
  module.exports = __toCommonJS(index_exports);
14114
14128
 
14115
- // ../core/dist/allowlist.js
14116
- init_config();
14117
- init_file_utils();
14118
- init_types2();
14119
-
14120
- // ../core/dist/url-utils.js
14121
- var import_node_crypto2 = require("node:crypto");
14129
+ // ../core/dist/allowlist-migration.js
14122
14130
  var import_node_path3 = require("node:path");
14123
- init_file_utils();
14124
- function normalizeUrl(raw) {
14125
- try {
14126
- const u = new URL(raw);
14127
- u.hash = "";
14128
- u.searchParams.sort();
14129
- return u.toString();
14130
- } catch {
14131
- 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;
14132
14144
  }
14133
- }
14134
- function hashCommand(command) {
14135
- return (0, import_node_crypto2.createHash)("sha256").update(command).digest("hex");
14136
- }
14137
- function normalizeFilePath(raw) {
14138
- const home = getHomeDir();
14139
- const expanded = raw.startsWith("~/") || raw === "~" ? `${home}${raw.slice(1)}` : raw;
14140
- return (0, import_node_path3.normalize)(expanded);
14145
+ return { ...entry, brand_key: brandKey };
14141
14146
  }
14142
14147
 
14143
- // ../core/dist/allowlist.js
14144
- function emptyAllowlist() {
14145
- return { urls: {}, commands: {}, filePaths: {} };
14146
- }
14147
- function parseEntries(raw) {
14148
- const entries = {};
14149
- for (const [key, entryData] of Object.entries(raw)) {
14150
- if (typeof entryData !== "object" || entryData === null)
14151
- continue;
14152
- const data = entryData;
14153
- if (typeof data.added_at === "string" && typeof data.reason === "string" && typeof data.original_verdict === "string") {
14154
- entries[key] = {
14155
- addedAt: data.added_at,
14156
- reason: data.reason,
14157
- originalVerdict: data.original_verdict
14158
- };
14159
- }
14160
- }
14161
- 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");
14162
14153
  }
14163
- async function loadAllowlist(config2, logger = nullLogger) {
14164
- const path = resolvePath(config2.path);
14165
- let raw;
14154
+ async function checkAllowlistMigration() {
14166
14155
  try {
14167
- 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 };
14168
14164
  } catch {
14169
- return emptyAllowlist();
14170
- }
14171
- let data;
14172
- try {
14173
- data = JSON.parse(raw);
14174
- } catch (e) {
14175
- logger.warn(`Failed to load allowlist from ${path}`, { error: String(e) });
14176
- return emptyAllowlist();
14177
- }
14178
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
14179
- logger.warn(`Allowlist file ${path} does not contain a JSON object`);
14180
- return emptyAllowlist();
14181
- }
14182
- const record = data;
14183
- const rawUrls = parseEntries(record.urls ?? {});
14184
- const urls = {};
14185
- for (const [key, entry] of Object.entries(rawUrls)) {
14186
- urls[normalizeUrl(key)] = entry;
14165
+ return { needed: false, entryTypes: [] };
14187
14166
  }
14188
- const rawFilePaths = parseEntries(record.file_paths ?? {});
14189
- const filePaths = {};
14190
- for (const [key, entry] of Object.entries(rawFilePaths)) {
14191
- filePaths[normalizeFilePath(key)] = entry;
14192
- }
14193
- return {
14194
- urls,
14195
- commands: parseEntries(record.commands ?? {}),
14196
- filePaths
14197
- };
14198
14167
  }
14199
- function isAllowlisted(allowlist, artifacts) {
14200
- for (const artifact of artifacts) {
14201
- if (artifact.type !== "command") {
14202
- continue;
14203
- }
14204
- const cmdHash = hashCommand(artifact.value);
14205
- if (cmdHash in allowlist.commands) {
14206
- return true;
14207
- }
14208
- }
14209
- for (const artifact of artifacts) {
14210
- if (artifact.type !== "file_path") {
14211
- continue;
14212
- }
14213
- if (normalizeFilePath(artifact.value) in allowlist.filePaths) {
14214
- return true;
14215
- }
14216
- }
14217
- if (artifacts.length > 0 && artifacts.every((artifact) => artifact.type === "url")) {
14218
- return artifacts.every((artifact) => normalizeUrl(artifact.value) in allowlist.urls);
14219
- }
14220
- 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");
14221
14177
  }
14222
14178
 
14223
14179
  // ../core/dist/approval-store.js
14224
- var import_node_crypto3 = require("node:crypto");
14180
+ var import_node_crypto2 = require("node:crypto");
14225
14181
  var PENDING_STALE_MS = 60 * 60 * 1e3;
14226
14182
  var APPROVED_TTL_MS = 10 * 60 * 1e3;
14227
- var ApprovalStore = class _ApprovalStore {
14183
+ var ApprovalStore = class {
14228
14184
  pending = /* @__PURE__ */ new Map();
14229
14185
  approved = /* @__PURE__ */ new Map();
14230
14186
  static actionId(toolName, params, sessionId) {
14231
14187
  const payload = JSON.stringify({ toolName, params, sessionId });
14232
- return (0, import_node_crypto3.createHash)("sha256").update(payload).digest("hex");
14188
+ return (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex").slice(0, 32);
14233
14189
  }
14234
14190
  static artifactId(type, value) {
14235
14191
  return `${type}:${value}`;
@@ -14237,6 +14193,9 @@ var ApprovalStore = class _ApprovalStore {
14237
14193
  setPending(actionId, entry) {
14238
14194
  this.pending.set(actionId, { ...entry, artifacts: [...entry.artifacts] });
14239
14195
  }
14196
+ getPending(actionId) {
14197
+ return this.pending.get(actionId) ?? null;
14198
+ }
14240
14199
  deletePending(actionId) {
14241
14200
  this.pending.delete(actionId);
14242
14201
  }
@@ -14263,40 +14222,6 @@ var ApprovalStore = class _ApprovalStore {
14263
14222
  }
14264
14223
  return true;
14265
14224
  }
14266
- hasApprovedArtifact(type, value) {
14267
- return this.findApprovedAction(type, value) !== null;
14268
- }
14269
- consumeApprovedArtifact(type, value) {
14270
- const id = _ApprovalStore.artifactId(type, value);
14271
- for (const [actionId, entry] of this.approved.entries()) {
14272
- if (Date.now() >= entry.expiresAt) {
14273
- this.approved.delete(actionId);
14274
- continue;
14275
- }
14276
- const idx = entry.artifacts.findIndex((a) => _ApprovalStore.artifactId(a.type, a.value) === id);
14277
- if (idx !== -1) {
14278
- entry.artifacts.splice(idx, 1);
14279
- if (entry.artifacts.length === 0) {
14280
- this.approved.delete(actionId);
14281
- }
14282
- return true;
14283
- }
14284
- }
14285
- return false;
14286
- }
14287
- findApprovedAction(type, value) {
14288
- const id = _ApprovalStore.artifactId(type, value);
14289
- for (const [actionId, entry] of this.approved.entries()) {
14290
- if (Date.now() >= entry.expiresAt) {
14291
- this.approved.delete(actionId);
14292
- continue;
14293
- }
14294
- if (entry.artifacts.some((artifact) => _ApprovalStore.artifactId(artifact.type, artifact.value) === id)) {
14295
- return actionId;
14296
- }
14297
- }
14298
- return null;
14299
- }
14300
14225
  cleanup(now = Date.now()) {
14301
14226
  for (const [actionId, entry] of this.pending.entries()) {
14302
14227
  if (now - entry.createdAt >= PENDING_STALE_MS) {
@@ -14312,60 +14237,125 @@ var ApprovalStore = class _ApprovalStore {
14312
14237
  };
14313
14238
 
14314
14239
  // ../core/dist/audit-log.js
14315
- var import_node_crypto4 = require("node:crypto");
14240
+ var import_node_crypto3 = require("node:crypto");
14241
+ init_config();
14242
+ init_file_utils();
14243
+
14244
+ // ../core/dist/jsonl-log-writer.js
14316
14245
  var import_promises = require("node:fs/promises");
14317
14246
  var import_node_path4 = require("node:path");
14247
+ var import_promises2 = require("node:timers/promises");
14318
14248
  init_config();
14319
- init_file_utils();
14320
- var MAX_SUMMARY_LEN = 200;
14321
- var AUDIT_LOG_SCHEMA_VERSION = 1;
14322
- function toolInputSummary(toolName, toolInput) {
14323
- if (toolName === "Bash") {
14324
- return String(toolInput.command ?? "").slice(0, MAX_SUMMARY_LEN);
14325
- }
14326
- if (toolName === "WebFetch") {
14327
- return String(toolInput.url ?? "").slice(0, MAX_SUMMARY_LEN);
14328
- }
14329
- if (toolName === "Write" || toolName === "Edit" || toolName === "Read" || toolName === "Delete") {
14330
- return String(toolInput.file_path ?? "").slice(0, MAX_SUMMARY_LEN);
14331
- }
14332
- return JSON.stringify(toolInput).slice(0, MAX_SUMMARY_LEN);
14333
- }
14334
- async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
14249
+ var writeQueues = /* @__PURE__ */ new Map();
14250
+ var ROTATE_LOCK_TIMEOUT_MS = 250;
14251
+ var ROTATE_LOCK_STALE_MS = 3e4;
14252
+ var ROTATE_LOCK_POLL_MS = 50;
14253
+ async function shouldRotate(filePath, maxBytes, maxFiles) {
14335
14254
  if (maxBytes <= 0 || maxFiles <= 0)
14336
- return;
14337
- let size;
14255
+ return false;
14338
14256
  try {
14339
14257
  const s = await (0, import_promises.stat)(filePath);
14340
- size = s.size;
14258
+ return s.size >= maxBytes;
14341
14259
  } catch {
14342
- return;
14260
+ return false;
14343
14261
  }
14344
- if (size < maxBytes)
14345
- return;
14262
+ }
14263
+ async function removeStaleRotateLock(lockPath) {
14346
14264
  try {
14347
- await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
14265
+ const s = await (0, import_promises.stat)(lockPath);
14266
+ if (Date.now() - s.mtimeMs < ROTATE_LOCK_STALE_MS)
14267
+ return;
14268
+ await (0, import_promises.rmdir)(lockPath);
14348
14269
  } catch {
14349
14270
  }
14350
- for (let i = maxFiles - 1; i >= 1; i--) {
14271
+ }
14272
+ async function acquireRotateLock(filePath) {
14273
+ const lockPath = `${filePath}.lock`;
14274
+ const deadline = Date.now() + ROTATE_LOCK_TIMEOUT_MS;
14275
+ while (true) {
14351
14276
  try {
14352
- await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14277
+ await (0, import_promises.mkdir)(lockPath);
14278
+ return async () => {
14279
+ try {
14280
+ await (0, import_promises.rmdir)(lockPath);
14281
+ } catch {
14282
+ }
14283
+ };
14353
14284
  } catch {
14285
+ await removeStaleRotateLock(lockPath);
14286
+ const remainingMs = deadline - Date.now();
14287
+ if (remainingMs <= 0)
14288
+ return void 0;
14289
+ await (0, import_promises2.setTimeout)(Math.min(ROTATE_LOCK_POLL_MS, remainingMs));
14354
14290
  }
14355
14291
  }
14292
+ }
14293
+ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
14294
+ if (!await shouldRotate(filePath, maxBytes, maxFiles))
14295
+ return;
14296
+ const releaseLock = await acquireRotateLock(filePath);
14297
+ if (!releaseLock)
14298
+ return;
14356
14299
  try {
14357
- await (0, import_promises.rename)(filePath, `${filePath}.1`);
14358
- } catch {
14300
+ if (!await shouldRotate(filePath, maxBytes, maxFiles))
14301
+ return;
14302
+ try {
14303
+ await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
14304
+ } catch {
14305
+ }
14306
+ for (let i = maxFiles - 1; i >= 1; i--) {
14307
+ try {
14308
+ await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14309
+ } catch {
14310
+ }
14311
+ }
14312
+ try {
14313
+ await (0, import_promises.rename)(filePath, `${filePath}.1`);
14314
+ } catch {
14315
+ }
14316
+ } finally {
14317
+ await releaseLock();
14359
14318
  }
14360
14319
  }
14361
- async function appendEntry(config2, entry) {
14362
- const path = resolvePath(config2.path);
14320
+ async function appendJsonlEntryNow(path, config2, entry) {
14363
14321
  await (0, import_promises.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
14364
14322
  await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
14365
- const stamped = { ...entry, schema_version: AUDIT_LOG_SCHEMA_VERSION };
14366
- await (0, import_promises.appendFile)(path, `${JSON.stringify(stamped)}
14323
+ await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
14367
14324
  `);
14368
14325
  }
14326
+ async function appendJsonlEntry(config2, entry) {
14327
+ const path = resolvePath(config2.path);
14328
+ const previousWrite = writeQueues.get(path) ?? Promise.resolve();
14329
+ const nextWrite = previousWrite.catch(() => {
14330
+ }).then(() => appendJsonlEntryNow(path, config2, entry));
14331
+ writeQueues.set(path, nextWrite);
14332
+ try {
14333
+ await nextWrite;
14334
+ } finally {
14335
+ if (writeQueues.get(path) === nextWrite)
14336
+ writeQueues.delete(path);
14337
+ }
14338
+ }
14339
+
14340
+ // ../core/dist/audit-log.js
14341
+ var MAX_SUMMARY_LEN = 200;
14342
+ var AUDIT_LOG_SCHEMA_VERSION = 1;
14343
+ function toolInputSummary(toolName, toolInput) {
14344
+ if (toolName === "Bash") {
14345
+ return String(toolInput.command ?? "").slice(0, MAX_SUMMARY_LEN);
14346
+ }
14347
+ if (toolName === "WebFetch") {
14348
+ return String(toolInput.url ?? "").slice(0, MAX_SUMMARY_LEN);
14349
+ }
14350
+ if (toolName === "Write" || toolName === "Edit" || toolName === "Read" || toolName === "Delete") {
14351
+ return String(toolInput.file_path ?? "").slice(0, MAX_SUMMARY_LEN);
14352
+ }
14353
+ return JSON.stringify(toolInput).slice(0, MAX_SUMMARY_LEN);
14354
+ }
14355
+ async function appendEntry(config2, entry) {
14356
+ const stamped = { ...entry, schema_version: AUDIT_LOG_SCHEMA_VERSION };
14357
+ await appendJsonlEntry(config2, stamped);
14358
+ }
14369
14359
  async function logVerdict(config2, input) {
14370
14360
  if (!config2.enabled)
14371
14361
  return;
@@ -14375,7 +14365,7 @@ async function logVerdict(config2, input) {
14375
14365
  return;
14376
14366
  const entry = {
14377
14367
  type: "runtime_verdict",
14378
- entry_id: input.eventId ?? (0, import_node_crypto4.randomUUID)(),
14368
+ entry_id: input.eventId ?? (0, import_node_crypto3.randomUUID)(),
14379
14369
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14380
14370
  session_id: input.sessionId,
14381
14371
  conversation_id: input.conversationId ?? input.sessionId,
@@ -14417,31 +14407,29 @@ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
14417
14407
  }
14418
14408
  }
14419
14409
 
14420
- // ../core/dist/brands.js
14421
- var defaultBranding = { name: "Sage", short_name: "Sage" };
14422
- var BRANDS = {
14423
- norton: { name: "Norton AI Agent Protection", short_name: "Norton" }
14424
- };
14425
- function resolveBranding(brandKey, logger) {
14426
- if (!brandKey)
14427
- return defaultBranding;
14428
- const entry = BRANDS[brandKey];
14429
- if (!entry) {
14430
- logger?.warn(`Unknown brand_key "${brandKey}" in config \u2014 using default branding`);
14431
- return defaultBranding;
14432
- }
14433
- return { ...entry, brand_key: brandKey };
14434
- }
14435
-
14436
14410
  // ../core/dist/cache.js
14437
14411
  init_config();
14438
14412
  init_file_utils();
14439
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
14440
14429
  var ONE_HOUR = 3600;
14441
14430
  var TWENTY_FOUR_HOURS = 86400;
14442
- var FAR_FUTURE = "9999-12-31T23:59:59+00:00";
14443
14431
  var VerdictCache = class {
14444
- store = { urls: {}, commands: {}, packages: {} };
14432
+ store = { urls: {}, packages: {} };
14445
14433
  path;
14446
14434
  config;
14447
14435
  logger;
@@ -14460,11 +14448,10 @@ var VerdictCache = class {
14460
14448
  const data = JSON.parse(raw);
14461
14449
  this.store = {
14462
14450
  urls: data.urls ?? {},
14463
- commands: data.commands ?? {},
14464
14451
  packages: data.packages ?? {}
14465
14452
  };
14466
14453
  } catch {
14467
- this.store = { urls: {}, commands: {}, packages: {} };
14454
+ this.store = { urls: {}, packages: {} };
14468
14455
  }
14469
14456
  }
14470
14457
  getUrl(url) {
@@ -14496,42 +14483,19 @@ var VerdictCache = class {
14496
14483
  sageVersion: this.version
14497
14484
  };
14498
14485
  }
14499
- getCommand(commandHash) {
14486
+ getPackage(key) {
14500
14487
  if (!this.config.enabled)
14501
14488
  return null;
14502
- const entry = this.store.commands[commandHash];
14489
+ const entry = this.store.packages[key];
14503
14490
  if (!entry || this.isStale(entry))
14504
14491
  return null;
14505
14492
  return {
14506
14493
  verdict: entry.verdict,
14507
14494
  severity: entry.severity,
14508
14495
  reasons: entry.reasons,
14509
- source: entry.source
14510
- };
14511
- }
14512
- putCommand(commandHash, verdict) {
14513
- if (!this.config.enabled)
14514
- return;
14515
- const { urlSignalLabels: _ignoredUrlLabels, ...rest } = verdict;
14516
- const now = /* @__PURE__ */ new Date();
14517
- this.store.commands[commandHash] = {
14518
- ...rest,
14519
- checkedAt: now.toISOString(),
14520
- expiresAt: FAR_FUTURE,
14521
- sageVersion: this.version
14522
- };
14523
- }
14524
- getPackage(key) {
14525
- if (!this.config.enabled)
14526
- return null;
14527
- const entry = this.store.packages[key];
14528
- if (!entry || this.isStale(entry))
14529
- return null;
14530
- return {
14531
- verdict: entry.verdict,
14532
- severity: entry.severity,
14533
- reasons: entry.reasons,
14534
- source: entry.source
14496
+ source: entry.source,
14497
+ ...entry.packageVerdict !== void 0 ? { packageVerdict: entry.packageVerdict } : {},
14498
+ ...entry.packageConfidence !== void 0 ? { packageConfidence: entry.packageConfidence } : {}
14535
14499
  };
14536
14500
  }
14537
14501
  putPackage(key, verdict, packageAgeDays) {
@@ -14581,10 +14545,6 @@ var VerdictCache = class {
14581
14545
  if (this.isStale(entry))
14582
14546
  delete this.store.urls[key];
14583
14547
  }
14584
- for (const [key, entry] of Object.entries(this.store.commands)) {
14585
- if (this.isStale(entry))
14586
- delete this.store.commands[key];
14587
- }
14588
14548
  for (const [key, entry] of Object.entries(this.store.packages)) {
14589
14549
  if (this.isStale(entry))
14590
14550
  delete this.store.packages[key];
@@ -14873,8 +14833,10 @@ var PersistentPowershellAmsiBackend = class {
14873
14833
  logger;
14874
14834
  process = null;
14875
14835
  available = false;
14836
+ invalidResultLogged = false;
14876
14837
  stdoutBuffer = "";
14877
14838
  pendingResponse = null;
14839
+ scanQueue = Promise.resolve();
14878
14840
  constructor(logger) {
14879
14841
  this.logger = logger;
14880
14842
  }
@@ -14890,6 +14852,19 @@ var PersistentPowershellAmsiBackend = class {
14890
14852
  this.pendingResponse = { resolve: resolve9, reject, timer };
14891
14853
  });
14892
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
+ }
14893
14868
  async init() {
14894
14869
  try {
14895
14870
  this.process = spawn("powershell.exe", [
@@ -14962,26 +14937,35 @@ var PersistentPowershellAmsiBackend = class {
14962
14937
  async scanString(content, contentName) {
14963
14938
  if (!this.available || !this.process)
14964
14939
  return null;
14965
- const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
14966
- try {
14967
- const req = JSON.stringify({ content: truncated, contentName });
14968
- 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}
14969
14948
  `);
14970
- const line = await this.waitForLine(PS_TIMEOUT);
14971
- const amsiResult = parseInt(line, 10);
14972
- if (Number.isNaN(amsiResult) || amsiResult < 0) {
14973
- this.logger.warn("AMSI: PowerShell scan returned invalid result", {
14974
- stdout: line.slice(0, 100),
14975
- contentName
14976
- });
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;
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();
14977
14966
  return null;
14978
14967
  }
14979
- this.logger.debug("AMSI: PowerShell scan result", { contentName, amsiResult });
14980
- return interpretAmsiResult(amsiResult, content, contentName);
14981
- } catch (e) {
14982
- this.logger.warn("AMSI: PowerShell scan failed", { error: String(e), contentName });
14983
- return null;
14984
- }
14968
+ });
14985
14969
  }
14986
14970
  close() {
14987
14971
  if (this.process) {
@@ -15007,6 +14991,7 @@ var PersistentPowershellAmsiBackend = class {
15007
14991
  var WslPowershellAmsiBackend = class {
15008
14992
  logger;
15009
14993
  available = false;
14994
+ invalidResultLogged = false;
15010
14995
  constructor(logger) {
15011
14996
  this.logger = logger;
15012
14997
  }
@@ -15050,10 +15035,13 @@ var WslPowershellAmsiBackend = class {
15050
15035
  clearTimeout(timer);
15051
15036
  const amsiResult = parseInt(stdout.trim(), 10);
15052
15037
  if (Number.isNaN(amsiResult) || amsiResult < 0) {
15053
- this.logger.warn("AMSI: PowerShell one-shot invalid result", {
15054
- stdout: stdout.slice(0, 100),
15055
- contentName
15056
- });
15038
+ if (!this.invalidResultLogged) {
15039
+ this.invalidResultLogged = true;
15040
+ this.logger.warn("AMSI: PowerShell one-shot invalid result", {
15041
+ stdout: stdout.slice(0, 100),
15042
+ contentName
15043
+ });
15044
+ }
15057
15045
  resolve9(null);
15058
15046
  return;
15059
15047
  }
@@ -15212,7 +15200,7 @@ init_file_utils();
15212
15200
  var import_meta = {};
15213
15201
  function resolveVersion() {
15214
15202
  if (true)
15215
- return "0.9.0";
15203
+ return "0.11.0";
15216
15204
  try {
15217
15205
  const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15218
15206
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -15392,9 +15380,6 @@ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
15392
15380
  const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
15393
15381
  return required.filter((name) => !isModelPresent(name, schema, sageDir));
15394
15382
  }
15395
- function anyRequiredModelMissing(schema = MODEL_SCHEMA_VERSION, sageDir) {
15396
- return missingRequiredModels(schema, sageDir).length > 0;
15397
- }
15398
15383
 
15399
15384
  // ../core/dist/clients/model-downloader.js
15400
15385
  init_types2();
@@ -15763,9 +15748,11 @@ var BundledPiProvider = class _BundledPiProvider {
15763
15748
  * crash (exit code 134/SIGABRT). Only exits if the runtime was actually
15764
15749
  * loaded.
15765
15750
  */
15766
- static exitIfModelLoaded() {
15767
- if (_BundledPiProvider.modelRuntimeLoaded)
15768
- process.exit(0);
15751
+ static async exitIfModelLoaded(logger) {
15752
+ if (!_BundledPiProvider.modelRuntimeLoaded)
15753
+ return;
15754
+ await logger?.flush?.();
15755
+ process.exit(0);
15769
15756
  }
15770
15757
  constructor(options = {}) {
15771
15758
  this.modelPath = options.modelPath ?? getModelDir("pi-model");
@@ -15859,6 +15846,8 @@ ${err.stack}` : String(err);
15859
15846
  */
15860
15847
  async classifyChunk(text) {
15861
15848
  const tok = this.tokenizer;
15849
+ if (!tok)
15850
+ throw new Error("PI tokenizer is not loaded");
15862
15851
  const encoded = tok.call(text, {
15863
15852
  truncation: true,
15864
15853
  max_length: MAX_TOKENS,
@@ -15891,6 +15880,8 @@ ${err.stack}` : String(err);
15891
15880
  */
15892
15881
  chunkText(text) {
15893
15882
  const tok = this.tokenizer;
15883
+ if (!tok)
15884
+ throw new Error("PI tokenizer is not loaded");
15894
15885
  const encoded = tok.tokenize(text, {
15895
15886
  addSpecialTokens: false,
15896
15887
  returnOffsetMapping: true,
@@ -15898,6 +15889,8 @@ ${err.stack}` : String(err);
15898
15889
  });
15899
15890
  const tokenIds = encoded.input_ids;
15900
15891
  const offsets = encoded.offset_mapping;
15892
+ if (!offsets)
15893
+ return [text];
15901
15894
  if (tokenIds.length <= MAX_TOKENS)
15902
15895
  return [text];
15903
15896
  const chunks = [];
@@ -16029,6 +16022,75 @@ var SkillCheckClient = class {
16029
16022
  // ../core/dist/index.js
16030
16023
  init_config();
16031
16024
 
16025
+ // ../core/dist/config-diagnostics.js
16026
+ init_config();
16027
+ init_file_utils();
16028
+ init_types2();
16029
+ function formatPath(path) {
16030
+ return path.replace(/\\/g, "/");
16031
+ }
16032
+ function configWarning(path, reason) {
16033
+ return { file: "config", path, reason };
16034
+ }
16035
+ function exceptionsWarning(path, reason) {
16036
+ return { file: "exceptions", path, reason };
16037
+ }
16038
+ function inspectConfig(raw, path) {
16039
+ let data;
16040
+ try {
16041
+ data = JSON.parse(raw);
16042
+ } catch {
16043
+ return configWarning(path, "parse");
16044
+ }
16045
+ return ConfigSchema.safeParse(data).success ? null : configWarning(path, "validation");
16046
+ }
16047
+ function inspectExceptions(raw, path) {
16048
+ let data;
16049
+ try {
16050
+ data = JSON.parse(raw);
16051
+ } catch {
16052
+ return exceptionsWarning(path, "parse");
16053
+ }
16054
+ return ExceptionsFileSchema.safeParse(data).success ? null : exceptionsWarning(path, "validation");
16055
+ }
16056
+ function getConfigurationWarningsSync(configPath, logger) {
16057
+ const warnings = [];
16058
+ const resolvedConfigPath = configPath ? resolvePath(configPath) : defaultConfigPath();
16059
+ try {
16060
+ const rawConfig = getFileContentSync(resolvedConfigPath);
16061
+ const warning = inspectConfig(rawConfig, resolvedConfigPath);
16062
+ if (warning)
16063
+ warnings.push(warning);
16064
+ } catch {
16065
+ }
16066
+ const config2 = loadConfigSync(resolvedConfigPath, logger);
16067
+ const exceptionsPath = resolvePath(config2.exceptions.path);
16068
+ try {
16069
+ const rawExceptions = getFileContentSync(exceptionsPath);
16070
+ const warning = inspectExceptions(rawExceptions, exceptionsPath);
16071
+ if (warning)
16072
+ warnings.push(warning);
16073
+ } catch {
16074
+ }
16075
+ return warnings;
16076
+ }
16077
+ function formatConfigurationWarnings(warnings, branding2) {
16078
+ if (warnings.length === 0)
16079
+ return null;
16080
+ const lines = [`\u26A0\uFE0F ${branding2.name}: configuration warning`];
16081
+ for (const warning of warnings) {
16082
+ if (warning.file === "config") {
16083
+ const reason = warning.reason === "parse" ? "is not valid JSON" : "has the wrong shape";
16084
+ lines.push(`- ${formatPath(warning.path)} ${reason}. ${branding2.short_name} is using default settings until this file is fixed.`);
16085
+ } else if (warning.reason === "parse") {
16086
+ lines.push(`- ${formatPath(warning.path)} is not valid JSON. Exception rules are not being applied until this file is fixed.`);
16087
+ } else {
16088
+ lines.push(`- ${formatPath(warning.path)} has the wrong shape. Expected ${JSON.stringify({ rules: [] })}. Exception rules are not being applied until this file is fixed.`);
16089
+ }
16090
+ }
16091
+ return lines.join("\n");
16092
+ }
16093
+
16032
16094
  // ../core/dist/content-policy.js
16033
16095
  var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
16034
16096
  ".py",
@@ -16271,7 +16333,7 @@ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {})
16271
16333
  }
16272
16334
 
16273
16335
  // ../core/dist/extended-info.js
16274
- var import_promises2 = require("node:fs/promises");
16336
+ var import_promises3 = require("node:fs/promises");
16275
16337
  var import_node_os3 = require("node:os");
16276
16338
  var import_node_path10 = require("node:path");
16277
16339
  init_file_utils();
@@ -16285,6 +16347,23 @@ var cache = /* @__PURE__ */ new Map();
16285
16347
  function isPlainObjectValue(value) {
16286
16348
  return typeof value === "object" && value !== null && !Array.isArray(value);
16287
16349
  }
16350
+ function sanitizeScalarLeaf(leafValue) {
16351
+ if (typeof leafValue === "string") {
16352
+ return safeTruncate(leafValue, EXTENDED_INFO_MAX_LEAF_CHARS);
16353
+ }
16354
+ return leafValue;
16355
+ }
16356
+ function sanitizeArrayLeaf(groupKey, leafKey, items, logger) {
16357
+ const out = [];
16358
+ for (const [index, item] of items.entries()) {
16359
+ if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
16360
+ logger.debug(`extended-info: dropped non-scalar array item '${groupKey}.${leafKey}[${index}]' (type ${typeof item})`);
16361
+ continue;
16362
+ }
16363
+ out.push(sanitizeScalarLeaf(item));
16364
+ }
16365
+ return out;
16366
+ }
16288
16367
  function sanitizeLeaves(groupKey, rawGroup, logger) {
16289
16368
  const entries = Object.entries(rawGroup);
16290
16369
  const overflow = entries.length > EXTENDED_INFO_MAX_KEYS_PER_GROUP;
@@ -16295,11 +16374,15 @@ function sanitizeLeaves(groupKey, rawGroup, logger) {
16295
16374
  const out = {};
16296
16375
  for (const [leafKey, leafValue] of retained) {
16297
16376
  if (typeof leafValue === "string") {
16298
- out[leafKey] = safeTruncate(leafValue, EXTENDED_INFO_MAX_LEAF_CHARS);
16377
+ out[leafKey] = sanitizeScalarLeaf(leafValue);
16299
16378
  continue;
16300
16379
  }
16301
16380
  if (typeof leafValue === "number" || typeof leafValue === "boolean") {
16302
- out[leafKey] = leafValue;
16381
+ out[leafKey] = sanitizeScalarLeaf(leafValue);
16382
+ continue;
16383
+ }
16384
+ if (Array.isArray(leafValue)) {
16385
+ out[leafKey] = sanitizeArrayLeaf(groupKey, leafKey, leafValue, logger);
16303
16386
  continue;
16304
16387
  }
16305
16388
  logger.debug(`extended-info: dropped non-scalar leaf '${groupKey}.${leafKey}' (type ${typeof leafValue})`);
@@ -16330,7 +16413,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
16330
16413
  async function loadExtendedInfoUncached(filePath, logger) {
16331
16414
  let size;
16332
16415
  try {
16333
- const info = await (0, import_promises2.stat)(filePath);
16416
+ const info = await (0, import_promises3.stat)(filePath);
16334
16417
  if (!info.isFile()) {
16335
16418
  logger.debug(`extended-info: not a regular file at ${filePath}`);
16336
16419
  return null;
@@ -16367,6 +16450,26 @@ async function loadExtendedInfoUncached(filePath, logger) {
16367
16450
  }
16368
16451
  return sanitizeDocument(parsed, logger);
16369
16452
  }
16453
+ function cloneExtendedInfoLeaf(leafValue) {
16454
+ return Array.isArray(leafValue) ? dedupeArrayItems(leafValue) : leafValue;
16455
+ }
16456
+ function cloneExtendedInfoGroup(groupValue) {
16457
+ return Object.fromEntries(Object.entries(groupValue).map(([leafKey, leafValue]) => [
16458
+ leafKey,
16459
+ cloneExtendedInfoLeaf(leafValue)
16460
+ ]));
16461
+ }
16462
+ function dedupeArrayItems(items) {
16463
+ const seen = /* @__PURE__ */ new Set();
16464
+ const out = [];
16465
+ for (const item of items) {
16466
+ if (seen.has(item))
16467
+ continue;
16468
+ seen.add(item);
16469
+ out.push(item);
16470
+ }
16471
+ return out;
16472
+ }
16370
16473
  function mergeExtendedInfo(envelope, extendedInfo) {
16371
16474
  const out = { ...envelope };
16372
16475
  if (!extendedInfo)
@@ -16374,7 +16477,7 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16374
16477
  for (const [groupKey, groupValue] of Object.entries(extendedInfo)) {
16375
16478
  const existing = out[groupKey];
16376
16479
  if (existing === void 0 || existing === null) {
16377
- out[groupKey] = { ...groupValue };
16480
+ out[groupKey] = cloneExtendedInfoGroup(groupValue);
16378
16481
  continue;
16379
16482
  }
16380
16483
  if (!isPlainObjectValue(existing)) {
@@ -16384,7 +16487,11 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16384
16487
  for (const [leafKey, leafValue] of Object.entries(groupValue)) {
16385
16488
  const current = merged[leafKey];
16386
16489
  if (current === void 0 || current === null) {
16387
- merged[leafKey] = leafValue;
16490
+ merged[leafKey] = cloneExtendedInfoLeaf(leafValue);
16491
+ continue;
16492
+ }
16493
+ if (Array.isArray(current) && Array.isArray(leafValue)) {
16494
+ merged[leafKey] = dedupeArrayItems([...current, ...leafValue]);
16388
16495
  }
16389
16496
  }
16390
16497
  out[groupKey] = merged;
@@ -16393,8 +16500,8 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16393
16500
  }
16394
16501
 
16395
16502
  // ../core/dist/installation-id.js
16396
- var import_node_crypto5 = require("node:crypto");
16397
- var import_promises3 = require("node:fs/promises");
16503
+ var import_node_crypto4 = require("node:crypto");
16504
+ var import_promises4 = require("node:fs/promises");
16398
16505
  var import_node_path11 = require("node:path");
16399
16506
  init_config();
16400
16507
  init_file_utils();
@@ -16411,9 +16518,9 @@ async function getInstallationId(sageDirPath) {
16411
16518
  } catch {
16412
16519
  }
16413
16520
  try {
16414
- const id = (0, import_node_crypto5.randomUUID)();
16415
- await (0, import_promises3.mkdir)(sageDir, { recursive: true, mode: 448 });
16416
- await (0, import_promises3.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16521
+ const id = (0, import_node_crypto4.randomUUID)();
16522
+ await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
16523
+ await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16417
16524
  return id;
16418
16525
  } catch (err) {
16419
16526
  if (err.code === "EEXIST") {
@@ -16491,37 +16598,46 @@ async function sendCommunityIqDetection(args) {
16491
16598
  signal: AbortSignal.timeout(timeoutMs)
16492
16599
  });
16493
16600
  if (!response.ok) {
16494
- logger.debug(`Detection telemetry HTTP ${response.status}`);
16601
+ logger.warn("Detection telemetry send failed", {
16602
+ eventId: args.eventId,
16603
+ status: response.status
16604
+ });
16605
+ } else {
16606
+ logger.debug("Detection telemetry sent", {
16607
+ eventId: args.eventId,
16608
+ toolName: args.toolName,
16609
+ hookType: args.hookType ?? "PreToolUse"
16610
+ });
16495
16611
  }
16496
16612
  } catch (err) {
16497
- logger.debug(`Detection telemetry send failed: ${err}`);
16613
+ logger.warn("Detection telemetry send failed", {
16614
+ eventId: args.eventId,
16615
+ error: String(err)
16616
+ });
16498
16617
  }
16499
16618
  }
16500
16619
 
16501
- // ../core/dist/engine.js
16620
+ // ../core/dist/policy.js
16502
16621
  init_types2();
16503
- var CONFIDENCE_THRESHOLD = 0.85;
16504
- var SENSITIVITY_THRESHOLDS = {
16505
- paranoid: 0.7,
16506
- balanced: 0.85,
16507
- relaxed: 0.95
16508
- };
16509
- var SEVERITY_MAP = {
16510
- critical: "critical",
16511
- high: "warning",
16512
- medium: "warning",
16513
- low: "info"
16514
- };
16515
- var ACTION_MAP = {
16516
- block: "deny",
16517
- require_approval: "ask",
16518
- log: "allow"
16519
- };
16520
- var DECISION_PRIORITY = {
16521
- allow: 0,
16522
- ask: 1,
16523
- 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 }
16524
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();
16525
16641
  var PI_LABEL_ONLY_SUFFIXES = /* @__PURE__ */ new Set([
16526
16642
  "command",
16527
16643
  "stdout",
@@ -16552,29 +16668,25 @@ function formatPiFinding(result) {
16552
16668
  return `${where} (${score})`;
16553
16669
  }
16554
16670
  var DecisionEngine = class {
16555
- threshold;
16556
- sensitivity;
16557
- constructor(sensitivity = "balanced") {
16558
- this.threshold = SENSITIVITY_THRESHOLDS[sensitivity] ?? CONFIDENCE_THRESHOLD;
16559
- 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;
16560
16679
  }
16561
16680
  async decide(sources) {
16562
16681
  const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds);
16563
16682
  if (signals.length === 0) {
16564
16683
  return this.allowVerdict();
16565
16684
  }
16566
- signals.sort((a, b) => (DECISION_PRIORITY[b.decision] ?? 0) - (DECISION_PRIORITY[a.decision] ?? 0));
16567
- const top = signals[0];
16568
- if (!top) {
16569
- return this.allowVerdict();
16570
- }
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);
16571
16688
  const allArtifacts = [...new Map(signals.map((s) => [s.artifact, s.artifact])).values()];
16572
16689
  const allReasons = [...new Map(signals.map((s) => [s.reason, s.reason])).values()];
16573
- const maxConfidence = Math.max(...signals.map((s) => s.confidence));
16574
- let decision = top.decision;
16575
- if (decision === "deny" && maxConfidence < this.threshold && top.source !== "pi_check") {
16576
- decision = "ask";
16577
- }
16578
16690
  return {
16579
16691
  decision,
16580
16692
  category: top.category,
@@ -16589,24 +16701,26 @@ var DecisionEngine = class {
16589
16701
  collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults, piCheckResults, piThresholds) {
16590
16702
  const signals = [];
16591
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;
16592
16707
  signals.push({
16593
- decision: ACTION_MAP[match.threat.action] ?? "ask",
16594
- category: match.threat.category,
16595
- confidence: match.threat.confidence,
16596
- severity: SEVERITY_MAP[match.threat.severity] ?? "warning",
16708
+ category,
16709
+ confidence,
16710
+ severity,
16597
16711
  source: "heuristic",
16598
- threatId: match.threat.id,
16599
- reason: match.threat.title,
16712
+ threatId: id,
16713
+ reason: title,
16600
16714
  artifact: match.artifact
16601
16715
  });
16602
16716
  }
16603
16717
  for (const result of urlCheckResults) {
16604
16718
  if (result.isMalicious) {
16719
+ const confidence = 1;
16605
16720
  const findingDetails = result.findings.map((f) => `${f.severityName}/${f.typeName}`).join(", ");
16606
16721
  signals.push({
16607
- decision: "deny",
16608
16722
  category: "network_egress",
16609
- confidence: 1,
16723
+ confidence,
16610
16724
  severity: "critical",
16611
16725
  source: "url_check",
16612
16726
  threatId: null,
@@ -16628,7 +16742,6 @@ var DecisionEngine = class {
16628
16742
  for (const result of amsiCheckResults) {
16629
16743
  if (result.isDetected) {
16630
16744
  signals.push({
16631
- decision: "deny",
16632
16745
  category: "malware",
16633
16746
  confidence: 1,
16634
16747
  severity: "critical",
@@ -16639,7 +16752,6 @@ var DecisionEngine = class {
16639
16752
  });
16640
16753
  } else if (result.isBlockedByAdmin) {
16641
16754
  signals.push({
16642
- decision: "deny",
16643
16755
  category: "malware",
16644
16756
  confidence: 0.9,
16645
16757
  severity: "critical",
@@ -16654,24 +16766,21 @@ var DecisionEngine = class {
16654
16766
  if (piCheckResults) {
16655
16767
  const highRisk = piThresholds?.highRisk ?? DEFAULT_PI_HIGH_RISK_THRESHOLD;
16656
16768
  const mediumRisk = piThresholds?.mediumRisk ?? DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
16657
- const suppressMediumPi = this.sensitivity === "relaxed";
16658
16769
  for (const result of piCheckResults) {
16659
16770
  if (result.risk >= highRisk) {
16660
16771
  signals.push({
16661
- decision: "deny",
16662
16772
  category: "prompt_injection",
16663
- confidence: result.risk,
16773
+ confidence: 1,
16664
16774
  severity: "critical",
16665
16775
  source: "pi_check",
16666
16776
  threatId: "PROMPT_INJECTION",
16667
16777
  reason: buildPiReason("Prompt injection detected", result),
16668
16778
  artifact: result.contentName
16669
16779
  });
16670
- } else if (!suppressMediumPi && result.risk >= mediumRisk) {
16780
+ } else if (result.risk >= mediumRisk) {
16671
16781
  signals.push({
16672
- decision: "ask",
16673
16782
  category: "prompt_injection",
16674
- confidence: result.risk,
16783
+ confidence: 0.6,
16675
16784
  severity: "warning",
16676
16785
  source: "pi_check",
16677
16786
  threatId: "PROMPT_INJECTION",
@@ -16686,21 +16795,10 @@ var DecisionEngine = class {
16686
16795
  packageVerdictToSignal(pkg) {
16687
16796
  switch (pkg.verdict) {
16688
16797
  case "not_found":
16689
- return {
16690
- decision: "deny",
16691
- category: "supply_chain",
16692
- confidence: 0.95,
16693
- severity: "critical",
16694
- source: "package_check",
16695
- threatId: null,
16696
- reason: pkg.details,
16697
- artifact: pkg.packageName
16698
- };
16699
16798
  case "malicious":
16700
16799
  return {
16701
- decision: "deny",
16702
16800
  category: "supply_chain",
16703
- confidence: 1,
16801
+ confidence: pkg.confidence,
16704
16802
  severity: "critical",
16705
16803
  source: "package_check",
16706
16804
  threatId: null,
@@ -16708,21 +16806,10 @@ var DecisionEngine = class {
16708
16806
  artifact: pkg.packageName
16709
16807
  };
16710
16808
  case "suspicious_age":
16711
- return {
16712
- decision: "ask",
16713
- category: "supply_chain",
16714
- confidence: 0.75,
16715
- severity: "warning",
16716
- source: "package_check",
16717
- threatId: null,
16718
- reason: pkg.details,
16719
- artifact: pkg.packageName
16720
- };
16721
16809
  case "unknown":
16722
16810
  return {
16723
- decision: "ask",
16724
16811
  category: "supply_chain",
16725
- confidence: 0.6,
16812
+ confidence: pkg.confidence,
16726
16813
  severity: "warning",
16727
16814
  source: "package_check",
16728
16815
  threatId: null,
@@ -16737,7 +16824,7 @@ var DecisionEngine = class {
16737
16824
  return {
16738
16825
  decision: "allow",
16739
16826
  category: "none",
16740
- confidence: 1,
16827
+ confidence: 0,
16741
16828
  severity: "info",
16742
16829
  source: "none",
16743
16830
  artifacts: [],
@@ -16748,32 +16835,32 @@ var DecisionEngine = class {
16748
16835
  };
16749
16836
 
16750
16837
  // ../core/dist/evaluator.js
16751
- var import_node_crypto7 = require("node:crypto");
16838
+ var import_node_crypto6 = require("node:crypto");
16752
16839
  init_config();
16753
16840
 
16754
16841
  // ../core/dist/exceptions.js
16755
- var import_node_crypto6 = require("node:crypto");
16842
+ var import_node_crypto5 = require("node:crypto");
16756
16843
  var import_node_path13 = require("node:path");
16757
16844
  init_config();
16758
16845
  init_file_utils();
16759
16846
 
16760
16847
  // ../core/dist/trusted-domains.js
16761
- var import_promises4 = require("node:fs/promises");
16848
+ var import_promises5 = require("node:fs/promises");
16762
16849
  var import_node_path12 = require("node:path");
16763
16850
  var import_yaml = __toESM(require_dist(), 1);
16764
16851
  init_file_utils();
16765
16852
  init_types2();
16766
- async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
16853
+ async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
16767
16854
  let files;
16768
16855
  try {
16769
- files = (await (0, import_promises4.readdir)(allowlistsDir)).filter((f) => f.endsWith(".yaml")).sort();
16856
+ files = (await (0, import_promises5.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
16770
16857
  } catch {
16771
- logger.debug("Allowlists directory does not exist", { path: allowlistsDir });
16858
+ logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
16772
16859
  return [];
16773
16860
  }
16774
16861
  const domains = [];
16775
16862
  for (const filename of files) {
16776
- const filePath = (0, import_node_path12.join)(allowlistsDir, filename);
16863
+ const filePath = (0, import_node_path12.join)(trustedDomainsDir, filename);
16777
16864
  let content;
16778
16865
  try {
16779
16866
  content = await getFileContent(filePath);
@@ -16811,7 +16898,7 @@ async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
16811
16898
  domains.push({ domain: domain.toLowerCase(), reason });
16812
16899
  }
16813
16900
  }
16814
- logger.debug(`Loaded ${domains.length} trusted domains from ${allowlistsDir}`);
16901
+ logger.debug(`Loaded ${domains.length} trusted domains from ${trustedDomainsDir}`);
16815
16902
  return domains;
16816
16903
  }
16817
16904
  function extractDomain(url) {
@@ -16838,7 +16925,7 @@ init_types2();
16838
16925
  var MAX_RULES_WARNING = 100;
16839
16926
  var REGEX_TIMEOUT_MS = 50;
16840
16927
  function computeRuleId(decision, match, pattern) {
16841
- 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");
16842
16929
  return hash.slice(0, 8);
16843
16930
  }
16844
16931
  async function loadExceptions(config2, logger = nullLogger) {
@@ -16937,8 +17024,14 @@ async function addException(artifact, reason, exceptionsPath, logger = nullLogge
16937
17024
  reason
16938
17025
  };
16939
17026
  const existing = await loadExceptions(excConfig, logger);
16940
- if (existing.some((r) => r.id === rule.id))
17027
+ if (existing.some((r) => r.id === rule.id)) {
17028
+ logger.debug("Exception rule already exists", {
17029
+ ruleId: rule.id,
17030
+ decision: rule.decision,
17031
+ match: rule.match
17032
+ });
16941
17033
  return;
17034
+ }
16942
17035
  const rules = [...existing, rule];
16943
17036
  const path = resolvePath(excConfig.path);
16944
17037
  try {
@@ -16951,6 +17044,11 @@ async function addException(artifact, reason, exceptionsPath, logger = nullLogge
16951
17044
  ...r.reason !== void 0 ? { reason: r.reason } : {}
16952
17045
  }))
16953
17046
  });
17047
+ logger.info("Exception rule added", {
17048
+ ruleId: rule.id,
17049
+ decision: rule.decision,
17050
+ match: rule.match
17051
+ });
16954
17052
  } catch (e) {
16955
17053
  logger.warn("Failed to write exception rule", { error: String(e) });
16956
17054
  }
@@ -17405,7 +17503,7 @@ var PackageChecker = class {
17405
17503
  packageName: pkg.name,
17406
17504
  registry: pkg.registry,
17407
17505
  verdict: "not_found",
17408
- confidence: 0.9,
17506
+ confidence: 0.95,
17409
17507
  details: `Package '${pkg.name}@${pkg.version}' version not found on ${pkg.registry} (hallucinated or unpublished)`
17410
17508
  };
17411
17509
  }
@@ -17438,7 +17536,7 @@ var PackageChecker = class {
17438
17536
  packageName: pkg.name,
17439
17537
  registry: pkg.registry,
17440
17538
  verdict: "suspicious_age",
17441
- confidence: 0.75,
17539
+ confidence: 0.6,
17442
17540
  details: `Package '${pkg.name}' first published ${Math.floor(ageDays)} days ago (suspicious age)`,
17443
17541
  ageDays
17444
17542
  };
@@ -17871,7 +17969,7 @@ async function updateSessionStatus(sessionId, verdict) {
17871
17969
  }
17872
17970
 
17873
17971
  // ../core/dist/threat-loader.js
17874
- var import_promises5 = require("node:fs/promises");
17972
+ var import_promises6 = require("node:fs/promises");
17875
17973
  var import_node_path15 = require("node:path");
17876
17974
  var import_yaml2 = __toESM(require_dist(), 1);
17877
17975
  init_file_utils();
@@ -17881,7 +17979,6 @@ var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
17881
17979
  "category",
17882
17980
  "severity",
17883
17981
  "confidence",
17884
- "action",
17885
17982
  "pattern",
17886
17983
  "match_on",
17887
17984
  "title"
@@ -17906,7 +18003,7 @@ async function loadThreats(threatDir, logger = nullLogger) {
17906
18003
  const threats = [];
17907
18004
  let files;
17908
18005
  try {
17909
- files = (await (0, import_promises5.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
18006
+ files = (await (0, import_promises6.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
17910
18007
  } catch {
17911
18008
  logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
17912
18009
  return threats;
@@ -17949,8 +18046,8 @@ async function loadThreats(threatDir, logger = nullLogger) {
17949
18046
  continue;
17950
18047
  let compiledPattern;
17951
18048
  try {
17952
- const flags = record.case_insensitive === true ? "i" : "";
17953
- compiledPattern = new RegExp(record.pattern, flags);
18049
+ const flags2 = record.case_insensitive === true ? "i" : "";
18050
+ compiledPattern = new RegExp(record.pattern, flags2);
17954
18051
  } catch (e) {
17955
18052
  logger.warn(`Skipping threat ${record.id}: invalid regex pattern`, {
17956
18053
  error: String(e)
@@ -17959,19 +18056,28 @@ async function loadThreats(threatDir, logger = nullLogger) {
17959
18056
  }
17960
18057
  const rawMatchOn = record.match_on;
17961
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
+ }
17962
18068
  threats.push({
17963
18069
  id: record.id,
17964
18070
  version: typeof record.version === "number" ? record.version : void 0,
17965
18071
  category: record.category,
17966
18072
  severity: record.severity,
17967
- confidence: Number(record.confidence),
17968
- action: record.action,
18073
+ confidence,
17969
18074
  pattern: record.pattern,
17970
18075
  compiledPattern,
17971
18076
  matchOn,
17972
18077
  title: record.title,
17973
18078
  expiresAt: parseExpiresAt(record.expires_at),
17974
- revoked: false
18079
+ revoked: false,
18080
+ flags
17975
18081
  });
17976
18082
  }
17977
18083
  }
@@ -17979,33 +18085,6 @@ async function loadThreats(threatDir, logger = nullLogger) {
17979
18085
  return threats;
17980
18086
  }
17981
18087
 
17982
- // ../core/dist/tool-names.js
17983
- var CANONICAL_TOOLS = [
17984
- "Bash",
17985
- "WebFetch",
17986
- "Write",
17987
- "Edit",
17988
- "Read",
17989
- "Delete",
17990
- "ApplyPatch",
17991
- "Glob",
17992
- "Grep",
17993
- "List",
17994
- "CodeSearch",
17995
- "WebSearch",
17996
- "Question",
17997
- "Task",
17998
- "ReadLines",
17999
- "MCP",
18000
- "Unknown"
18001
- ];
18002
- var CANONICAL_SET = new Set(CANONICAL_TOOLS);
18003
- function canonicalizeToolName(map, rawName) {
18004
- if (CANONICAL_SET.has(rawName))
18005
- return rawName;
18006
- return map[rawName] ?? "Unknown";
18007
- }
18008
-
18009
18088
  // ../core/dist/evaluator.js
18010
18089
  init_types2();
18011
18090
  var AMSI_CONTENT_SNIPPET_MAX = 200;
@@ -18038,7 +18117,7 @@ function allowVerdict(source = "none") {
18038
18117
  return {
18039
18118
  decision: "allow",
18040
18119
  category: "none",
18041
- confidence: 1,
18120
+ confidence: 0,
18042
18121
  severity: "info",
18043
18122
  source,
18044
18123
  artifacts: [],
@@ -18046,11 +18125,48 @@ function allowVerdict(source = "none") {
18046
18125
  reasons: []
18047
18126
  };
18048
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
+ }
18133
+ function shouldSkipPromptInjectionForLocalMarkdown(request) {
18134
+ if (request.toolName !== "Write" && request.toolName !== "Edit")
18135
+ return false;
18136
+ const filePath = resolveFilePath(request.toolInput) ?? "";
18137
+ const trimmed = filePath.trim();
18138
+ return /\.(?:md|mdx|markdown|mdown|mkdn)$/i.test(trimmed);
18139
+ }
18049
18140
  async function evaluateToolCall(request, context) {
18050
18141
  const logger = context.logger ?? nullLogger;
18051
- const config2 = await loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
18142
+ const config2 = await resolveEvaluationConfig(context, logger);
18143
+ const eventId = (0, import_node_crypto6.randomUUID)();
18144
+ logger.debug("Tool call evaluation started", {
18145
+ eventId,
18146
+ toolUseId: request.toolUseId,
18147
+ toolName: request.toolName,
18148
+ hookType: request.hookType,
18149
+ agentRuntime: request.agentRuntime,
18150
+ artifactsCount: request.artifacts.length
18151
+ });
18152
+ const logEvaluationCompleted = (verdict2) => {
18153
+ logger.debug("Tool call evaluation completed", {
18154
+ eventId,
18155
+ toolUseId: request.toolUseId,
18156
+ toolName: request.toolName,
18157
+ hookType: request.hookType,
18158
+ agentRuntime: request.agentRuntime,
18159
+ artifactsCount: request.artifacts.length,
18160
+ decision: verdict2.decision,
18161
+ source: verdict2.source,
18162
+ category: verdict2.category,
18163
+ severity: verdict2.severity
18164
+ });
18165
+ };
18052
18166
  if (request.artifacts.length === 0 && !config2.pi_check.enabled) {
18053
- return allowVerdict("no_artifacts");
18167
+ const verdict2 = allowVerdict("no_artifacts");
18168
+ logEvaluationCompleted(verdict2);
18169
+ return verdict2;
18054
18170
  }
18055
18171
  try {
18056
18172
  const exceptions = await loadExceptions(config2.exceptions, logger);
@@ -18076,30 +18192,15 @@ async function evaluateToolCall(request, context) {
18076
18192
  verdict: verdict2,
18077
18193
  conversationId: request.conversationId,
18078
18194
  agentRuntime: request.agentRuntime,
18079
- hookType: request.hookType
18195
+ hookType: request.hookType,
18196
+ eventId,
18197
+ toolUseId: request.toolUseId
18080
18198
  });
18081
18199
  } catch {
18082
18200
  }
18201
+ logEvaluationCompleted(verdict2);
18083
18202
  return verdict2;
18084
18203
  }
18085
- try {
18086
- const allowlist = await loadAllowlist(config2.allowlist, logger);
18087
- if (isAllowlisted(allowlist, request.artifacts)) {
18088
- const allowV = allowVerdict("allowlisted");
18089
- await logVerdict(config2.logging, {
18090
- sessionId: request.sessionId,
18091
- toolName: request.toolName,
18092
- toolInput: request.toolInput,
18093
- verdict: allowV,
18094
- userOverride: true,
18095
- conversationId: request.conversationId,
18096
- agentRuntime: request.agentRuntime,
18097
- hookType: request.hookType
18098
- });
18099
- return allowV;
18100
- }
18101
- } catch {
18102
- }
18103
18204
  const allowMatch = findAllowException(exceptions, request.artifacts);
18104
18205
  if (allowMatch) {
18105
18206
  const allowV = allowVerdict("exception");
@@ -18112,19 +18213,24 @@ async function evaluateToolCall(request, context) {
18112
18213
  userOverride: true,
18113
18214
  conversationId: request.conversationId,
18114
18215
  agentRuntime: request.agentRuntime,
18115
- hookType: request.hookType
18216
+ hookType: request.hookType,
18217
+ eventId,
18218
+ toolUseId: request.toolUseId
18116
18219
  });
18117
18220
  } catch {
18118
18221
  }
18222
+ logEvaluationCompleted(allowV);
18119
18223
  return allowV;
18120
18224
  }
18121
- } catch {
18225
+ } catch (error) {
18226
+ logger.debug("Exception checks failed open", { error: String(error) });
18122
18227
  }
18123
18228
  let cache2 = null;
18124
18229
  try {
18125
18230
  cache2 = new VerdictCache(config2.cache, logger, VERSION);
18126
18231
  await cache2.load();
18127
- } catch {
18232
+ } catch (error) {
18233
+ logger.debug("Verdict cache initialization failed open", { error: String(error) });
18128
18234
  cache2 = null;
18129
18235
  }
18130
18236
  const urls = request.artifacts.filter((artifact) => artifact.type === "url").map((artifact) => artifact.value);
@@ -18136,7 +18242,10 @@ async function evaluateToolCall(request, context) {
18136
18242
  const disabledSet = new Set(config2.disabled_threats);
18137
18243
  threats = threats.filter((t) => !disabledSet.has(t.id));
18138
18244
  }
18139
- const trustedDomains = await loadTrustedDomains(context.allowlistsDir, logger);
18245
+ if (shouldSkipPromptInjectionForLocalMarkdown(request)) {
18246
+ threats = threats.filter((t) => t.category !== "prompt_injection");
18247
+ }
18248
+ const trustedDomains = await loadTrustedDomains(context.trustedDomainsDir, logger);
18140
18249
  const heuristics = new HeuristicsEngine(threats, trustedDomains);
18141
18250
  heuristicMatches = heuristics.match(request.artifacts);
18142
18251
  }
@@ -18153,10 +18262,19 @@ async function evaluateToolCall(request, context) {
18153
18262
  const amsiCheckResults = [];
18154
18263
  if (config2.amsi_check.enabled && isAmsiSupported()) {
18155
18264
  let amsiClient = null;
18265
+ let amsiLease = null;
18266
+ let ownsAmsiClient = false;
18156
18267
  try {
18157
- amsiClient = new AmsiClient(logger);
18158
- await amsiClient.init();
18159
- 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;
18160
18278
  const scans = [];
18161
18279
  if (request.toolName === "Bash") {
18162
18280
  const command = request.toolInput.command ?? "";
@@ -18187,7 +18305,7 @@ async function evaluateToolCall(request, context) {
18187
18305
  }
18188
18306
  }
18189
18307
  for (const scan of scans) {
18190
- const result = await amsiClient.scanString(scan.scanType, scan.name, scan.content);
18308
+ const result = await activeAmsiClient.scanString(scan.scanType, scan.name, scan.content);
18191
18309
  if (result) {
18192
18310
  amsiCheckResults.push(result);
18193
18311
  }
@@ -18195,16 +18313,26 @@ async function evaluateToolCall(request, context) {
18195
18313
  }
18196
18314
  } catch {
18197
18315
  } finally {
18198
- amsiClient?.close();
18316
+ try {
18317
+ if (amsiLease) {
18318
+ await amsiLease.release();
18319
+ } else if (ownsAmsiClient) {
18320
+ amsiClient?.close();
18321
+ }
18322
+ } catch {
18323
+ }
18199
18324
  }
18200
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
+ });
18201
18333
  const allPiResults = [];
18202
- const piDenySignals = [];
18203
- const heuristicPiDeny = heuristicMatches.some((m) => m.threat.action === "block" && m.threat.category === "prompt_injection");
18204
- const heuristicDeny = heuristicMatches.some((m) => m.threat.action === "block");
18205
- const urlAlreadyDenied = cachedUrlVerdicts.size > 0 && [...cachedUrlVerdicts.values()].some((v) => v.verdict !== "allow");
18206
- const urlCheckDenied = urlCheckResults.some((r) => r.isMalicious);
18207
- 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") {
18208
18336
  try {
18209
18337
  const url = extractWebFetchUrl(request.toolInput);
18210
18338
  if (url) {
@@ -18220,9 +18348,6 @@ async function evaluateToolCall(request, context) {
18220
18348
  const result = await provider.checkContent(fetched.content, `WebFetch:${url}`);
18221
18349
  if (result) {
18222
18350
  allPiResults.push(result);
18223
- if (result.risk >= config2.pi_check.high_risk_threshold) {
18224
- piDenySignals.push(result);
18225
- }
18226
18351
  }
18227
18352
  }
18228
18353
  }
@@ -18231,34 +18356,41 @@ async function evaluateToolCall(request, context) {
18231
18356
  } catch {
18232
18357
  }
18233
18358
  }
18234
- const engine = new DecisionEngine(config2.sensitivity);
18235
- let verdict = await engine.decide({
18236
- heuristicMatches,
18237
- urlCheckResults,
18238
- packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
18239
- amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
18240
- piCheckResults: piDenySignals.length > 0 ? piDenySignals : void 0,
18241
- piThresholds: {
18242
- highRisk: config2.pi_check.high_risk_threshold,
18243
- mediumRisk: config2.pi_check.medium_risk_threshold
18244
- }
18245
- });
18246
- 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") {
18247
18376
  for (const [url, cachedVerdict] of cachedUrlVerdicts) {
18248
18377
  if (cachedVerdict.verdict === "allow") {
18249
18378
  continue;
18250
18379
  }
18251
- verdict = {
18252
- decision: cachedVerdict.verdict,
18253
- category: "network_egress",
18254
- confidence: 1,
18255
- severity: cachedVerdict.severity,
18256
- source: `cache(${cachedVerdict.source})`,
18257
- artifacts: [url],
18258
- matchedThreatId: null,
18259
- reasons: cachedVerdict.reasons
18260
- };
18261
- 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
+ }
18262
18394
  }
18263
18395
  }
18264
18396
  await cacheUrlResults(urlCheckResults, cache2);
@@ -18339,7 +18471,6 @@ async function evaluateToolCall(request, context) {
18339
18471
  auditSignals.amsi_checks = relevantAmsi.map((r) => buildAmsiSignal(r));
18340
18472
  }
18341
18473
  }
18342
- const eventId = (0, import_node_crypto7.randomUUID)();
18343
18474
  const resolvedSignals = Object.keys(auditSignals).length > 0 ? auditSignals : void 0;
18344
18475
  const builtContent = buildContentSnapshot(request.toolName, request.toolInput, request.artifacts, auditSignals);
18345
18476
  const resolvedContent = Object.keys(builtContent).length > 0 ? builtContent : void 0;
@@ -18357,7 +18488,8 @@ async function evaluateToolCall(request, context) {
18357
18488
  eventId,
18358
18489
  toolUseId: request.toolUseId
18359
18490
  });
18360
- } catch {
18491
+ } catch (error) {
18492
+ logger.debug("Audit verdict logging failed open", { error: String(error) });
18361
18493
  }
18362
18494
  if (verdict.decision === "deny") {
18363
18495
  try {
@@ -18372,18 +18504,21 @@ async function evaluateToolCall(request, context) {
18372
18504
  communityIqEnabled: config2.community_iq,
18373
18505
  logger
18374
18506
  });
18375
- } catch {
18507
+ } catch (error) {
18508
+ logger.debug("Detection telemetry failed open", { error: String(error) });
18376
18509
  }
18377
18510
  }
18378
18511
  if (verdict.decision !== "allow") {
18379
18512
  try {
18380
18513
  await updateSessionStatus(request.sessionId, verdict);
18381
- } catch {
18514
+ } catch (error) {
18515
+ logger.debug("Session status update failed open", { error: String(error) });
18382
18516
  }
18383
18517
  }
18384
18518
  const piWarnings = allPiResults.filter((r) => r.risk >= config2.pi_check.medium_risk_threshold && r.risk < config2.pi_check.high_risk_threshold);
18385
18519
  if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
18386
18520
  verdict.piWarnings = piWarnings;
18521
+ logEvaluationCompleted(verdict);
18387
18522
  return verdict;
18388
18523
  }
18389
18524
  function partitionUrlsByCache(urls, cache2) {
@@ -18453,21 +18588,41 @@ async function checkPackages(request, config2, cache2, logger) {
18453
18588
  }
18454
18589
  if (!parsedPackages || parsedPackages.length === 0)
18455
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]);
18456
18594
  const uncached = [];
18457
18595
  for (const pkg of parsedPackages) {
18458
18596
  const cacheKey2 = `${pkg.registry}:${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`;
18459
18597
  const cached = cache2?.getPackage(cacheKey2);
18460
- if (cached && cached.verdict !== "allow") {
18461
- results.push({
18462
- packageName: pkg.name,
18463
- registry: pkg.registry,
18464
- verdict: cached.verdict === "deny" ? "malicious" : "suspicious_age",
18465
- confidence: 1,
18466
- 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
18467
18615
  });
18468
- } else if (!cached) {
18469
18616
  uncached.push(pkg);
18617
+ continue;
18470
18618
  }
18619
+ results.push({
18620
+ packageName: pkg.name,
18621
+ registry: pkg.registry,
18622
+ verdict: rawVerdict,
18623
+ confidence: rawConf,
18624
+ details: cached.reasons.join("; ")
18625
+ });
18471
18626
  }
18472
18627
  if (uncached.length > 0) {
18473
18628
  const checker = new PackageChecker({
@@ -18487,7 +18642,9 @@ async function checkPackages(request, config2, cache2, logger) {
18487
18642
  verdict: result.verdict === "clean" ? "allow" : isCritical ? "deny" : "ask",
18488
18643
  severity: isCritical ? "critical" : "warning",
18489
18644
  reasons: [result.details],
18490
- source: "package_check"
18645
+ source: "package_check",
18646
+ packageVerdict: result.verdict,
18647
+ packageConfidence: result.confidence
18491
18648
  }, result.ageDays ?? null);
18492
18649
  }
18493
18650
  }
@@ -18513,9 +18670,9 @@ var PAD = 12;
18513
18670
  var SEPARATOR_WIDTH = 48;
18514
18671
  function severityEmoji(severity) {
18515
18672
  const s = severity.toLowerCase();
18516
- if (s === "critical" || s === "high")
18673
+ if (s === "critical")
18517
18674
  return "\u{1F6A8}";
18518
- if (s === "medium" || s === "warn" || s === "warning")
18675
+ if (s === "warning")
18519
18676
  return "\u26A0\uFE0F";
18520
18677
  return "\u2139\uFE0F";
18521
18678
  }
@@ -18536,20 +18693,20 @@ ${formatUpdateNotice(versionCheck)}`;
18536
18693
  }
18537
18694
  return base;
18538
18695
  }
18539
- function totalHighCrit(results) {
18696
+ function totalNonInfo(results) {
18540
18697
  let n = 0;
18541
18698
  for (const r of results) {
18542
18699
  for (const f of r.findings) {
18543
- if (f.severity === "critical" || f.severity === "high")
18700
+ if (f.severity !== "info")
18544
18701
  n++;
18545
18702
  }
18546
18703
  }
18547
18704
  return n;
18548
18705
  }
18549
- function findFirstHighCrit(results) {
18706
+ function findFirstNonInfo(results) {
18550
18707
  for (const r of results) {
18551
18708
  for (const f of r.findings) {
18552
- if (f.severity === "critical" || f.severity === "high") {
18709
+ if (f.severity !== "info") {
18553
18710
  return { plugin: r.plugin, finding: f };
18554
18711
  }
18555
18712
  }
@@ -18562,10 +18719,10 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18562
18719
  const MAX_FINDINGS = 5;
18563
18720
  let first = true;
18564
18721
  for (const result of results) {
18565
- const highCrit = result.findings.filter((f) => f.severity === "critical" || f.severity === "high");
18566
- if (highCrit.length === 0)
18722
+ const nonInfo = result.findings.filter((f) => f.severity !== "info");
18723
+ if (nonInfo.length === 0)
18567
18724
  continue;
18568
- for (const f of highCrit.slice(0, MAX_FINDINGS)) {
18725
+ for (const f of nonInfo.slice(0, MAX_FINDINGS)) {
18569
18726
  if (!first)
18570
18727
  lines.push("");
18571
18728
  first = false;
@@ -18577,8 +18734,6 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18577
18734
  lines.push(kv("Artifact", f.artifact));
18578
18735
  }
18579
18736
  lines.push(kv("Source", f.sourceFile));
18580
- const actionLabel = f.action === "block" ? "Blocked" : "Flagged";
18581
- lines.push(kv("Action", actionLabel));
18582
18737
  if (f.recommendations && f.recommendations.length > 0) {
18583
18738
  lines.push(kv("Recommendations", ""));
18584
18739
  for (const rec of f.recommendations) {
@@ -18586,7 +18741,7 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18586
18741
  }
18587
18742
  }
18588
18743
  }
18589
- const overflow = highCrit.length - MAX_FINDINGS;
18744
+ const overflow = nonInfo.length - MAX_FINDINGS;
18590
18745
  if (overflow > 0) {
18591
18746
  lines.push("");
18592
18747
  lines.push(` ... and ${overflow} more findings`);
@@ -18599,8 +18754,8 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18599
18754
  return lines.join("\n");
18600
18755
  }
18601
18756
  function formatThreatBannerCompact(version, results, versionCheck, branding2) {
18602
- const total = totalHighCrit(results);
18603
- const first = findFirstHighCrit(results);
18757
+ const total = totalNonInfo(results);
18758
+ const first = findFirstNonInfo(results);
18604
18759
  if (!first || total === 0) {
18605
18760
  return formatStartupClean(version, versionCheck, branding2);
18606
18761
  }
@@ -18665,43 +18820,162 @@ function truncateValue(value) {
18665
18820
  function summarizeArtifacts(artifacts) {
18666
18821
  if (artifacts.length === 0)
18667
18822
  return "none";
18668
- 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;
18669
18827
  }
18670
18828
  function formatDenyMessage(verdict, branding2 = defaultBranding) {
18671
- 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;
18672
18831
  return [
18673
18832
  `${branding2.name} blocked this action.`,
18674
18833
  `Severity: ${verdict.severity}`,
18675
18834
  `Category: ${verdict.category}`,
18676
- `Reason: ${reasons}`
18835
+ `Reason: ${reasons}`,
18836
+ `If this is a false positive, use the sage_report_false_positive MCP tool to report it.`
18677
18837
  ].join("\n");
18678
18838
  }
18679
18839
 
18680
- // ../core/dist/marketplace-migration.js
18681
- init_config();
18682
-
18683
18840
  // ../core/dist/model-download.js
18684
18841
  init_types2();
18685
18842
 
18843
+ // ../core/dist/operational-log.js
18844
+ var LEVEL_PRIORITY = {
18845
+ debug: 10,
18846
+ info: 20,
18847
+ warn: 30,
18848
+ error: 40
18849
+ };
18850
+ var FLUSH_TIMEOUT_MS = 250;
18851
+ function shouldLog(config2, level) {
18852
+ return config2.enabled && LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[config2.level];
18853
+ }
18854
+ function normalizeData(data) {
18855
+ if (!data)
18856
+ return void 0;
18857
+ const seen = /* @__PURE__ */ new WeakSet();
18858
+ try {
18859
+ return JSON.parse(JSON.stringify(data, (_key, value) => {
18860
+ if (typeof value === "bigint")
18861
+ return value.toString();
18862
+ if (value instanceof Error) {
18863
+ return { name: value.name, message: value.message, stack: value.stack };
18864
+ }
18865
+ if (value && typeof value === "object") {
18866
+ if (seen.has(value))
18867
+ return "[Circular]";
18868
+ seen.add(value);
18869
+ }
18870
+ return value;
18871
+ }));
18872
+ } catch {
18873
+ return { serialization_error: "Failed to serialize log data" };
18874
+ }
18875
+ }
18876
+ async function writeOperationalLogEntry(config2, entry) {
18877
+ if (!shouldLog(config2, entry.level))
18878
+ return;
18879
+ try {
18880
+ await appendJsonlEntry(config2, entry);
18881
+ } catch {
18882
+ }
18883
+ }
18884
+ function createOperationalLogger(config2, runtime) {
18885
+ const pendingWrites = /* @__PURE__ */ new Set();
18886
+ function log(component, level, message, data) {
18887
+ if (!shouldLog(config2, level))
18888
+ return;
18889
+ const entry = {
18890
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18891
+ level,
18892
+ runtime,
18893
+ component,
18894
+ message,
18895
+ ...data ? { data: normalizeData(data) } : {}
18896
+ };
18897
+ const write = writeOperationalLogEntry(config2, entry);
18898
+ pendingWrites.add(write);
18899
+ void write.finally(() => pendingWrites.delete(write));
18900
+ }
18901
+ async function flush() {
18902
+ const deadline = Date.now() + FLUSH_TIMEOUT_MS;
18903
+ while (pendingWrites.size > 0) {
18904
+ const remainingMs = deadline - Date.now();
18905
+ if (remainingMs <= 0)
18906
+ return;
18907
+ let timeout;
18908
+ try {
18909
+ const result = await Promise.race([
18910
+ Promise.allSettled([...pendingWrites]).then(() => "drained"),
18911
+ new Promise((resolve9) => {
18912
+ timeout = setTimeout(() => resolve9("timeout"), remainingMs);
18913
+ timeout.unref?.();
18914
+ })
18915
+ ]);
18916
+ if (result === "timeout")
18917
+ return;
18918
+ } finally {
18919
+ if (timeout)
18920
+ clearTimeout(timeout);
18921
+ }
18922
+ }
18923
+ }
18924
+ function forComponent(component) {
18925
+ return {
18926
+ debug(msg, data) {
18927
+ log(component, "debug", msg, data);
18928
+ },
18929
+ info(msg, data) {
18930
+ log(component, "info", msg, data);
18931
+ },
18932
+ warn(msg, data) {
18933
+ log(component, "warn", msg, data);
18934
+ },
18935
+ error(msg, data) {
18936
+ log(component, "error", msg, data);
18937
+ },
18938
+ flush
18939
+ };
18940
+ }
18941
+ return {
18942
+ debug(component, msg, data) {
18943
+ log(component, "debug", msg, data);
18944
+ },
18945
+ info(component, msg, data) {
18946
+ log(component, "info", msg, data);
18947
+ },
18948
+ warn(component, msg, data) {
18949
+ log(component, "warn", msg, data);
18950
+ },
18951
+ error(component, msg, data) {
18952
+ log(component, "error", msg, data);
18953
+ },
18954
+ forComponent,
18955
+ flush
18956
+ };
18957
+ }
18958
+
18686
18959
  // ../core/dist/plugin-scan-cache.js
18687
- var import_node_crypto8 = require("node:crypto");
18688
- var import_promises6 = require("node:fs/promises");
18960
+ var import_node_crypto7 = require("node:crypto");
18961
+ var import_promises7 = require("node:fs/promises");
18689
18962
  var import_node_path16 = require("node:path");
18690
18963
  init_file_utils();
18691
18964
  init_types2();
18692
18965
  var DEFAULT_CACHE_PATH = (0, import_node_path16.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
18693
- var CACHE_TTL_DAYS = 7;
18966
+ var CACHE_TTL_DAYS = 1;
18967
+ var SCHEMA_VERSION = 3;
18694
18968
  function cacheKey(pluginKey, version, lastUpdated) {
18695
18969
  return `${pluginKey}:${version}:${lastUpdated}`;
18696
18970
  }
18697
18971
  async function computeConfigHash(sageVersion, ...dirs) {
18698
- const h = (0, import_node_crypto8.createHash)("sha256");
18972
+ const h = (0, import_node_crypto7.createHash)("sha256");
18699
18973
  if (sageVersion)
18700
18974
  h.update(sageVersion);
18701
18975
  for (const dir of dirs) {
18702
18976
  let files;
18703
18977
  try {
18704
- files = (await (0, import_promises6.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
18978
+ files = (await (0, import_promises7.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
18705
18979
  } catch {
18706
18980
  continue;
18707
18981
  }
@@ -18724,9 +18998,17 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
18724
18998
  }
18725
18999
  try {
18726
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
+ }
18727
19009
  const storedHash = data.config_hash ?? "";
18728
19010
  if (configHash && storedHash !== configHash) {
18729
- logger.info("Config hash changed, invalidating plugin scan cache", {
19011
+ logger.debug("Config hash changed, invalidating plugin scan cache", {
18730
19012
  from: storedHash,
18731
19013
  to: configHash
18732
19014
  });
@@ -18751,6 +19033,7 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
18751
19033
  async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nullLogger) {
18752
19034
  try {
18753
19035
  const data = {
19036
+ schema_version: SCHEMA_VERSION,
18754
19037
  config_hash: cache2.configHash,
18755
19038
  entries: Object.fromEntries(Object.entries(cache2.entries).map(([key, entry]) => [
18756
19039
  key,
@@ -18797,14 +19080,15 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
18797
19080
  }
18798
19081
 
18799
19082
  // ../core/dist/plugin-scanner.js
18800
- var import_node_crypto10 = require("node:crypto");
18801
- var import_promises8 = require("node:fs/promises");
19083
+ var import_node_crypto9 = require("node:crypto");
19084
+ var import_promises9 = require("node:fs/promises");
18802
19085
  var import_node_path18 = require("node:path");
19086
+ init_config();
18803
19087
  init_file_utils();
18804
19088
 
18805
19089
  // ../core/dist/skill-id.js
18806
- var import_node_crypto9 = require("node:crypto");
18807
- var import_promises7 = require("node:fs/promises");
19090
+ var import_node_crypto8 = require("node:crypto");
19091
+ var import_promises8 = require("node:fs/promises");
18808
19092
  var import_node_path17 = require("node:path");
18809
19093
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
18810
19094
  function isContained(childReal, rootReal) {
@@ -18820,22 +19104,22 @@ function normalizePath(p) {
18820
19104
  }
18821
19105
  async function entriesFromDirectory(dirPath) {
18822
19106
  const absDir = (0, import_node_path17.resolve)(dirPath);
18823
- const rootReal = await (0, import_promises7.realpath)(absDir);
19107
+ const rootReal = await (0, import_promises8.realpath)(absDir);
18824
19108
  const entries = [];
18825
19109
  const visited = /* @__PURE__ */ new Set();
18826
19110
  async function walk(currentPath) {
18827
- const real = await (0, import_promises7.realpath)(currentPath);
19111
+ const real = await (0, import_promises8.realpath)(currentPath);
18828
19112
  if (visited.has(real))
18829
19113
  return;
18830
19114
  if (!isContained(real, rootReal))
18831
19115
  return;
18832
19116
  visited.add(real);
18833
- const items = await (0, import_promises7.readdir)(currentPath);
19117
+ const items = await (0, import_promises8.readdir)(currentPath);
18834
19118
  const dirs = [];
18835
19119
  const files = [];
18836
19120
  for (const item of items) {
18837
19121
  const fullPath = (0, import_node_path17.join)(currentPath, item);
18838
- const st = await (0, import_promises7.stat)(fullPath);
19122
+ const st = await (0, import_promises8.stat)(fullPath);
18839
19123
  if (st.isDirectory())
18840
19124
  dirs.push(item);
18841
19125
  else if (st.isFile())
@@ -18851,14 +19135,14 @@ async function entriesFromDirectory(dirPath) {
18851
19135
  }
18852
19136
  for (const f of files) {
18853
19137
  const fullPath = (0, import_node_path17.join)(currentPath, f);
18854
- const fileReal = await (0, import_promises7.realpath)(fullPath);
19138
+ const fileReal = await (0, import_promises8.realpath)(fullPath);
18855
19139
  if (!isContained(fileReal, rootReal))
18856
19140
  continue;
18857
19141
  const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
18858
19142
  entries.push({
18859
19143
  entryPath: relPath,
18860
19144
  isDir: false,
18861
- content: await (0, import_promises7.readFile)(fullPath)
19145
+ content: await (0, import_promises8.readFile)(fullPath)
18862
19146
  });
18863
19147
  }
18864
19148
  }
@@ -18881,14 +19165,14 @@ function computeSkillId(entries) {
18881
19165
  const fileHashes = {};
18882
19166
  const treeEntries = [];
18883
19167
  for (const e of normalized) {
18884
- 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");
18885
19169
  const entryType = e.isDir ? "dir" : "file";
18886
19170
  treeEntries.push({ type: entryType, entryPath: e.entryPath, hash: contentHash });
18887
19171
  if (!e.isDir)
18888
19172
  fileHashes[e.entryPath] = contentHash;
18889
19173
  }
18890
19174
  treeEntries.sort((a, b) => a.entryPath < b.entryPath ? -1 : a.entryPath > b.entryPath ? 1 : 0);
18891
- const skillHasher = (0, import_node_crypto9.createHash)("sha256");
19175
+ const skillHasher = (0, import_node_crypto8.createHash)("sha256");
18892
19176
  for (const e of treeEntries) {
18893
19177
  skillHasher.update(Buffer.from(`${e.type}\0${e.entryPath}\0${e.hash}
18894
19178
  `, "utf8"));
@@ -18899,10 +19183,10 @@ async function findSkillPackages(rootDir) {
18899
19183
  const found = [];
18900
19184
  let rootReal;
18901
19185
  try {
18902
- const rootStat = await (0, import_promises7.stat)(rootDir);
19186
+ const rootStat = await (0, import_promises8.stat)(rootDir);
18903
19187
  if (!rootStat.isDirectory())
18904
19188
  return found;
18905
- rootReal = await (0, import_promises7.realpath)((0, import_node_path17.resolve)(rootDir));
19189
+ rootReal = await (0, import_promises8.realpath)((0, import_node_path17.resolve)(rootDir));
18906
19190
  } catch {
18907
19191
  return found;
18908
19192
  }
@@ -18910,7 +19194,7 @@ async function findSkillPackages(rootDir) {
18910
19194
  async function walk(dir) {
18911
19195
  let real;
18912
19196
  try {
18913
- real = await (0, import_promises7.realpath)(dir);
19197
+ real = await (0, import_promises8.realpath)(dir);
18914
19198
  } catch {
18915
19199
  return;
18916
19200
  }
@@ -18921,14 +19205,14 @@ async function findSkillPackages(rootDir) {
18921
19205
  visited.add(real);
18922
19206
  let items;
18923
19207
  try {
18924
- items = await (0, import_promises7.readdir)(dir);
19208
+ items = await (0, import_promises8.readdir)(dir);
18925
19209
  } catch {
18926
19210
  return;
18927
19211
  }
18928
19212
  items.sort();
18929
19213
  if (items.includes("SKILL.md")) {
18930
19214
  try {
18931
- const st = await (0, import_promises7.stat)((0, import_node_path17.join)(dir, "SKILL.md"));
19215
+ const st = await (0, import_promises8.stat)((0, import_node_path17.join)(dir, "SKILL.md"));
18932
19216
  if (st.isFile())
18933
19217
  found.push(dir);
18934
19218
  } catch {
@@ -18940,7 +19224,7 @@ async function findSkillPackages(rootDir) {
18940
19224
  const fullPath = (0, import_node_path17.join)(dir, item);
18941
19225
  let st;
18942
19226
  try {
18943
- st = await (0, import_promises7.stat)(fullPath);
19227
+ st = await (0, import_promises8.stat)(fullPath);
18944
19228
  } catch {
18945
19229
  continue;
18946
19230
  }
@@ -18968,7 +19252,6 @@ async function computeSkillIdsForRoot(rootDir) {
18968
19252
 
18969
19253
  // ../core/dist/plugin-scanner.js
18970
19254
  init_types2();
18971
- var DEFAULT_PLUGINS_REGISTRY = (0, import_node_path18.join)(getHomeDir(), ".claude", "plugins", "installed_plugins.json");
18972
19255
  var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
18973
19256
  ".py",
18974
19257
  ".js",
@@ -18994,7 +19277,7 @@ async function walkPluginFiles(installPath, logger) {
18994
19277
  async function walk(dirOrFile) {
18995
19278
  let stats;
18996
19279
  try {
18997
- stats = await (0, import_promises8.stat)(dirOrFile);
19280
+ stats = await (0, import_promises9.stat)(dirOrFile);
18998
19281
  } catch {
18999
19282
  return;
19000
19283
  }
@@ -19007,7 +19290,7 @@ async function walkPluginFiles(installPath, logger) {
19007
19290
  if (stats.isDirectory()) {
19008
19291
  let entries;
19009
19292
  try {
19010
- entries = await (0, import_promises8.readdir)(dirOrFile);
19293
+ entries = await (0, import_promises9.readdir)(dirOrFile);
19011
19294
  } catch {
19012
19295
  return;
19013
19296
  }
@@ -19055,8 +19338,6 @@ async function scanPlugin(plugin, options = {}) {
19055
19338
  threatId: "AMSI_SCAN",
19056
19339
  title: `AMSI detection (result=${amsiResult.amsiResult})`,
19057
19340
  severity: "critical",
19058
- confidence: 1,
19059
- action: "block",
19060
19341
  artifact: content.slice(0, 200),
19061
19342
  sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
19062
19343
  });
@@ -19068,7 +19349,7 @@ async function scanPlugin(plugin, options = {}) {
19068
19349
  allUrls.push(...extractUrls(content));
19069
19350
  }
19070
19351
  if (checkFileHashes) {
19071
- 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");
19072
19353
  const existing = hashToFiles.get(sha256);
19073
19354
  if (existing) {
19074
19355
  existing.push(filePath);
@@ -19089,8 +19370,6 @@ async function scanPlugin(plugin, options = {}) {
19089
19370
  threatId: "URL_CHECK",
19090
19371
  title: `Malicious URL (${findingDetails})`,
19091
19372
  severity: "critical",
19092
- confidence: 1,
19093
- action: "block",
19094
19373
  artifact: ur.url.slice(0, 200),
19095
19374
  sourceFile: "URL check"
19096
19375
  });
@@ -19112,8 +19391,6 @@ async function scanPlugin(plugin, options = {}) {
19112
19391
  threatId: "FILE_CHECK",
19113
19392
  title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
19114
19393
  severity: "critical",
19115
- confidence: 1,
19116
- action: "block",
19117
19394
  artifact: fr.sha256,
19118
19395
  sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
19119
19396
  });
@@ -19141,13 +19418,11 @@ async function runSkillCheck(plugin, findings, logger) {
19141
19418
  const risk = (verdict.overallRiskLevel ?? "").toUpperCase();
19142
19419
  if (risk !== "HIGH" && risk !== "CRITICAL")
19143
19420
  continue;
19144
- const severity = risk === "CRITICAL" ? "critical" : "high";
19421
+ const severity = risk === "CRITICAL" ? "critical" : "warning";
19145
19422
  findings.push({
19146
19423
  threatId: "SKILL_CHECK",
19147
19424
  title: verdict.summary?.trim() || `Risky skill detected (${risk})`,
19148
19425
  severity,
19149
- confidence: 1,
19150
- action: "log",
19151
19426
  artifact: skillId.slice(0, 16),
19152
19427
  sourceFile: (0, import_node_path18.relative)(plugin.installPath, folder) || ".",
19153
19428
  recommendations: verdict.recommendations.length > 0 ? verdict.recommendations : void 0
@@ -19172,8 +19447,6 @@ function fromCachedFinding(finding) {
19172
19447
  threatId: finding.threat_id,
19173
19448
  title: finding.title,
19174
19449
  severity: finding.severity,
19175
- confidence: finding.confidence,
19176
- action: finding.action,
19177
19450
  artifact: finding.artifact,
19178
19451
  sourceFile: finding.source_file,
19179
19452
  recommendations: finding.recommendations
@@ -19184,22 +19457,29 @@ function toFindingData(finding) {
19184
19457
  threat_id: finding.threatId,
19185
19458
  title: finding.title,
19186
19459
  severity: finding.severity,
19187
- confidence: finding.confidence,
19188
- action: finding.action,
19189
19460
  artifact: finding.artifact,
19190
19461
  source_file: finding.sourceFile,
19191
19462
  recommendations: finding.recommendations
19192
19463
  };
19193
19464
  }
19194
19465
  function toAuditFindingData(finding) {
19195
- const { action: _, ...rest } = toFindingData(finding);
19196
- return rest;
19466
+ return { ...toFindingData(finding) };
19197
19467
  }
19198
19468
  async function runSessionStartScan(context) {
19199
19469
  const logger = context.logger ?? nullLogger;
19200
19470
  const sageConfig = await loadConfig(context.configPath, logger);
19201
19471
  const plugins = context.plugins;
19202
19472
  if (plugins.length === 0) {
19473
+ logger.debug("Session plugin scan completed", {
19474
+ totalPlugins: 0,
19475
+ resultsWithFindings: 0,
19476
+ findingsCount: 0,
19477
+ cacheHits: 0,
19478
+ cacheMisses: 0,
19479
+ scanned: 0,
19480
+ allowedByException: 0,
19481
+ deniedByException: 0
19482
+ });
19203
19483
  return [];
19204
19484
  }
19205
19485
  let exceptions = [];
@@ -19228,13 +19508,25 @@ async function runSessionStartScan(context) {
19228
19508
  }
19229
19509
  }
19230
19510
  async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient, logger) {
19231
- const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.allowlistsDir);
19511
+ const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.trustedDomainsDir);
19232
19512
  const cache2 = await loadScanCache(configHash, context.scanCachePath, logger);
19233
19513
  const resultsWithFindings = [];
19234
19514
  let cacheModified = false;
19515
+ const stats = {
19516
+ totalPlugins: plugins.length,
19517
+ cacheHitsClean: 0,
19518
+ cacheHitsWithFindings: 0,
19519
+ cacheMisses: 0,
19520
+ scanned: 0,
19521
+ allowedByException: 0,
19522
+ deniedByException: 0,
19523
+ findingsCount: 0
19524
+ };
19235
19525
  for (const plugin of plugins) {
19236
19526
  const denyMatch = findPluginDenyException(exceptions, plugin.key);
19237
19527
  if (denyMatch) {
19528
+ stats.deniedByException += 1;
19529
+ stats.findingsCount += 1;
19238
19530
  resultsWithFindings.push({
19239
19531
  plugin,
19240
19532
  findings: [
@@ -19242,8 +19534,6 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19242
19534
  threatId: "EXCEPTION-DENY",
19243
19535
  title: `Denied by exception: ${denyMatch.pattern}${denyMatch.reason ? ` \u2014 ${denyMatch.reason}` : ""}`,
19244
19536
  severity: "critical",
19245
- confidence: 1,
19246
- action: "block",
19247
19537
  artifact: plugin.key,
19248
19538
  sourceFile: "~/.sage/exceptions.json"
19249
19539
  }
@@ -19253,19 +19543,25 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19253
19543
  }
19254
19544
  const allowMatch = findPluginAllowException(exceptions, plugin.key);
19255
19545
  if (allowMatch) {
19546
+ stats.allowedByException += 1;
19256
19547
  continue;
19257
19548
  }
19258
19549
  const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
19259
19550
  if (cached && cached.findings.length === 0) {
19551
+ stats.cacheHitsClean += 1;
19260
19552
  continue;
19261
19553
  }
19262
19554
  if (cached && cached.findings.length > 0) {
19555
+ stats.cacheHitsWithFindings += 1;
19556
+ stats.findingsCount += cached.findings.length;
19263
19557
  resultsWithFindings.push({
19264
19558
  plugin,
19265
19559
  findings: cached.findings.map(fromCachedFinding)
19266
19560
  });
19267
19561
  continue;
19268
19562
  }
19563
+ stats.cacheMisses += 1;
19564
+ stats.scanned += 1;
19269
19565
  const result = await scanPlugin(plugin, {
19270
19566
  checkUrls: context.checkUrls ?? true,
19271
19567
  checkFileHashes: context.checkFileHashes ?? true,
@@ -19275,6 +19571,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19275
19571
  storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
19276
19572
  cacheModified = true;
19277
19573
  if (result.findings.length > 0) {
19574
+ stats.findingsCount += result.findings.length;
19278
19575
  resultsWithFindings.push(result);
19279
19576
  }
19280
19577
  }
@@ -19287,6 +19584,11 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19287
19584
  }
19288
19585
  } catch {
19289
19586
  }
19587
+ logger.debug("Session plugin scan completed", {
19588
+ ...stats,
19589
+ cacheHits: stats.cacheHitsClean + stats.cacheHitsWithFindings,
19590
+ resultsWithFindings: resultsWithFindings.length
19591
+ });
19290
19592
  return resultsWithFindings;
19291
19593
  }
19292
19594
 
@@ -19379,7 +19681,7 @@ async function runSessionStart(ctx) {
19379
19681
  runSessionStartScan({
19380
19682
  plugins: ctx.plugins,
19381
19683
  threatsDir: ctx.threatsDir,
19382
- allowlistsDir: ctx.allowlistsDir,
19684
+ trustedDomainsDir: ctx.trustedDomainsDir,
19383
19685
  sageVersion: ctx.version,
19384
19686
  logger,
19385
19687
  configPath: ctx.configPath,
@@ -19401,14 +19703,30 @@ async function runSessionStart(ctx) {
19401
19703
  return { scanResults, versionCheck };
19402
19704
  }
19403
19705
  async function maybeSpawnModelDownloadWorker(args) {
19404
- if (!args.workerPath)
19706
+ if (!args.workerPath) {
19707
+ args.logger.debug("Model download worker skipped", {
19708
+ result: "skipped",
19709
+ skippedReason: "missing_worker_path"
19710
+ });
19405
19711
  return;
19712
+ }
19406
19713
  if (!(0, import_node_fs4.existsSync)(args.workerPath)) {
19407
- args.logger.debug(`model download worker script not found at ${args.workerPath}`);
19714
+ args.logger.debug("Model download worker skipped", {
19715
+ result: "skipped",
19716
+ skippedReason: "worker_script_not_found",
19717
+ workerPath: args.workerPath
19718
+ });
19408
19719
  return;
19409
19720
  }
19410
- if (!anyRequiredModelMissing(MODEL_SCHEMA_VERSION, args.sageDirPath))
19721
+ const missingModels = missingRequiredModels(MODEL_SCHEMA_VERSION, args.sageDirPath);
19722
+ if (missingModels.length === 0) {
19723
+ args.logger.debug("Model download worker skipped", {
19724
+ result: "skipped",
19725
+ skippedReason: "no_missing_models",
19726
+ schema: MODEL_SCHEMA_VERSION
19727
+ });
19411
19728
  return;
19729
+ }
19412
19730
  let piEnabled = false;
19413
19731
  try {
19414
19732
  const config2 = await loadConfig(args.configPath, args.logger);
@@ -19416,9 +19734,16 @@ async function maybeSpawnModelDownloadWorker(args) {
19416
19734
  } catch {
19417
19735
  piEnabled = false;
19418
19736
  }
19419
- if (!piEnabled)
19737
+ if (!piEnabled) {
19738
+ args.logger.debug("Model download worker skipped", {
19739
+ result: "skipped",
19740
+ skippedReason: "pi_check_disabled",
19741
+ schema: MODEL_SCHEMA_VERSION,
19742
+ missingModels
19743
+ });
19420
19744
  return;
19421
- spawnModelDownloadWorker({
19745
+ }
19746
+ const spawned = spawnModelDownloadWorker({
19422
19747
  sageDirPath: args.sageDirPath,
19423
19748
  workerPath: args.workerPath,
19424
19749
  agentRuntime: args.agentRuntime,
@@ -19426,6 +19751,11 @@ async function maybeSpawnModelDownloadWorker(args) {
19426
19751
  versionApp: args.versionApp,
19427
19752
  logger: args.logger
19428
19753
  });
19754
+ args.logger.debug("Model download worker spawn completed", {
19755
+ result: spawned ? "spawned" : "failed",
19756
+ schema: MODEL_SCHEMA_VERSION,
19757
+ missingModels
19758
+ });
19429
19759
  }
19430
19760
  function spawnModelDownloadWorker(args) {
19431
19761
  const logger = args.logger ?? nullLogger;
@@ -19443,42 +19773,56 @@ function spawnModelDownloadWorker(args) {
19443
19773
  }
19444
19774
  });
19445
19775
  child.unref();
19776
+ return true;
19446
19777
  } catch (err) {
19447
- logger.debug(`failed to spawn model download worker: ${err}`);
19778
+ logger.warn("Failed to spawn model download worker", { error: String(err) });
19779
+ return false;
19448
19780
  }
19449
19781
  }
19450
19782
 
19451
19783
  // ../core/dist/scan-handler.js
19452
- async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose") {
19453
- logger.info(`${branding2.name} plugin scan started (${context})`, {
19784
+ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion) {
19785
+ logger.debug(`${branding2.name} plugin scan started (${context})`, {
19786
+ agentRuntime,
19787
+ pluginsCount: plugins.length,
19454
19788
  threatsDir,
19455
- allowlistsDir
19789
+ trustedDomainsDir
19456
19790
  });
19457
19791
  const result = await runSessionStart({
19458
19792
  plugins,
19459
19793
  threatsDir,
19460
- allowlistsDir,
19794
+ trustedDomainsDir,
19461
19795
  version,
19462
19796
  logger,
19463
19797
  agentRuntime,
19798
+ agentRuntimeVersion,
19464
19799
  modelDownloadWorkerPath
19465
19800
  });
19466
- logger.info(`${branding2.name} plugin scan (${context}) complete`, {
19467
- findings: result.scanResults.length,
19801
+ const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
19802
+ const completionData = {
19803
+ agentRuntime,
19804
+ pluginsCount: plugins.length,
19805
+ resultsWithFindings: result.scanResults.length,
19806
+ findingsCount,
19468
19807
  updateAvailable: result.versionCheck?.updateAvailable ?? false
19469
- });
19808
+ };
19809
+ if (findingsCount > 0) {
19810
+ logger.warn(`${branding2.name} plugin scan (${context}) complete with findings`, completionData);
19811
+ } else {
19812
+ logger.debug(`${branding2.name} plugin scan (${context}) complete`, completionData);
19813
+ }
19470
19814
  return formatSessionStartMessage(version, result, branding2, style);
19471
19815
  }
19472
19816
  function createScanHandler(options) {
19473
- 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;
19474
19818
  return async () => {
19475
19819
  try {
19476
19820
  let plugins = await discoverPlugins2();
19477
19821
  plugins = plugins.filter((p) => !p.key.startsWith(selfPrefix));
19478
19822
  if (plugins.length === 0) {
19479
- logger.warn(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
19823
+ logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
19480
19824
  }
19481
- 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);
19482
19826
  onResult?.(msg);
19483
19827
  } catch (e) {
19484
19828
  logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
@@ -19486,6 +19830,33 @@ function createScanHandler(options) {
19486
19830
  };
19487
19831
  }
19488
19832
 
19833
+ // ../core/dist/tool-names.js
19834
+ var CANONICAL_TOOLS = [
19835
+ "Bash",
19836
+ "WebFetch",
19837
+ "Write",
19838
+ "Edit",
19839
+ "Read",
19840
+ "Delete",
19841
+ "ApplyPatch",
19842
+ "Glob",
19843
+ "Grep",
19844
+ "List",
19845
+ "CodeSearch",
19846
+ "WebSearch",
19847
+ "Question",
19848
+ "Task",
19849
+ "ReadLines",
19850
+ "MCP",
19851
+ "Unknown"
19852
+ ];
19853
+ var CANONICAL_SET = new Set(CANONICAL_TOOLS);
19854
+ function canonicalizeToolName(map, rawName) {
19855
+ if (CANONICAL_SET.has(rawName))
19856
+ return rawName;
19857
+ return map[rawName] ?? "Unknown";
19858
+ }
19859
+
19489
19860
  // ../core/dist/index.js
19490
19861
  init_types2();
19491
19862
 
@@ -19495,7 +19866,7 @@ var packageRoot = (0, import_node_path19.resolve)(__dirname, "..");
19495
19866
  function getBundledDataDirs() {
19496
19867
  return {
19497
19868
  threatsDir: (0, import_node_path19.join)(packageRoot, "resources", "threats"),
19498
- allowlistsDir: (0, import_node_path19.join)(packageRoot, "resources", "allowlists")
19869
+ trustedDomainsDir: (0, import_node_path19.join)(packageRoot, "resources", "trusted-domains")
19499
19870
  };
19500
19871
  }
19501
19872
  var cachedVersion;
@@ -19510,42 +19881,21 @@ function getSageVersion() {
19510
19881
  return cachedVersion;
19511
19882
  }
19512
19883
 
19513
- // src/logger-adapter.ts
19514
- function createLogger(pluginLogger) {
19515
- function formatMessage(msg, data) {
19516
- return data ? `${msg} ${JSON.stringify(data)}` : msg;
19517
- }
19518
- return {
19519
- debug(msg, data) {
19520
- pluginLogger.debug?.(formatMessage(msg, data));
19521
- },
19522
- info(msg, data) {
19523
- pluginLogger.info(formatMessage(msg, data));
19524
- },
19525
- warn(msg, data) {
19526
- pluginLogger.warn(formatMessage(msg, data));
19527
- },
19528
- error(msg, data) {
19529
- pluginLogger.error(formatMessage(msg, data));
19530
- }
19531
- };
19532
- }
19533
-
19534
19884
  // src/startup-scan.ts
19535
19885
  var import_node_path21 = require("node:path");
19536
19886
 
19537
19887
  // src/plugin-discovery.ts
19538
- var import_promises9 = require("node:fs/promises");
19888
+ var import_promises10 = require("node:fs/promises");
19539
19889
  var import_node_os4 = require("node:os");
19540
19890
  var import_node_path20 = require("node:path");
19541
19891
  var DEFAULT_EXTENSIONS_DIR = (0, import_node_path20.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
19542
19892
  async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding) {
19543
- logger.info(`${branding2.name} plugin discovery: scanning extensions directory`, {
19893
+ logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
19544
19894
  path: extensionsDir
19545
19895
  });
19546
19896
  let entries;
19547
19897
  try {
19548
- entries = await (0, import_promises9.readdir)(extensionsDir);
19898
+ entries = await (0, import_promises10.readdir)(extensionsDir);
19549
19899
  } catch {
19550
19900
  logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
19551
19901
  return [];
@@ -19555,7 +19905,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
19555
19905
  const extDir = (0, import_node_path20.join)(extensionsDir, entry);
19556
19906
  let stats;
19557
19907
  try {
19558
- stats = await (0, import_promises9.stat)(extDir);
19908
+ stats = await (0, import_promises10.stat)(extDir);
19559
19909
  if (!stats.isDirectory()) continue;
19560
19910
  } catch {
19561
19911
  continue;
@@ -19583,13 +19933,13 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
19583
19933
  lastUpdated: stats.mtime.toISOString()
19584
19934
  });
19585
19935
  }
19586
- logger.info(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
19936
+ logger.debug(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
19587
19937
  return plugins;
19588
19938
  }
19589
19939
 
19590
19940
  // src/startup-scan.ts
19591
19941
  function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult) {
19592
- const { threatsDir, allowlistsDir } = getBundledDataDirs();
19942
+ const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
19593
19943
  const version = getSageVersion();
19594
19944
  return createScanHandler({
19595
19945
  logger,
@@ -19597,9 +19947,10 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
19597
19947
  discoverPlugins: () => discoverOpenClawPlugins(logger, void 0, branding2),
19598
19948
  selfPrefix: "@gendigital/sage-openclaw@",
19599
19949
  threatsDir,
19600
- allowlistsDir,
19950
+ trustedDomainsDir,
19601
19951
  version,
19602
19952
  agentRuntime: "openclaw",
19953
+ // No agentRuntimeVersion: the OpenClaw plugin API exposes no host version.
19603
19954
  branding: branding2,
19604
19955
  onResult,
19605
19956
  modelDownloadWorkerPath: (0, import_node_path21.resolve)(__dirname, "model-download-worker.cjs")
@@ -19616,7 +19967,7 @@ function createBeforeAgentStartHandler(getFindings, clearFindings, logger, brand
19616
19967
  const findings = getFindings();
19617
19968
  if (!findings) return void 0;
19618
19969
  clearFindings();
19619
- logger.info(`${branding2.name}: surfacing plugin scan findings via before_agent_start`);
19970
+ logger.debug(`${branding2.name}: surfacing plugin scan findings via before_agent_start`);
19620
19971
  const prependContext = [
19621
19972
  '<security-alert source="sage-plugin-scan">',
19622
19973
  findings,
@@ -19690,32 +20041,77 @@ function mapToolToArtifacts(toolName, params) {
19690
20041
  return null;
19691
20042
  }
19692
20043
  }
19693
- function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir, branding2 = defaultBranding) {
20044
+ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomainsDir, branding2 = defaultBranding) {
19694
20045
  async function handleToolCall(event, ctx) {
20046
+ const sessionId = ctx?.sessionKey ?? "unknown";
20047
+ const completeHook = (result, data = {}) => {
20048
+ logger.debug("OpenClaw tool hook completed", {
20049
+ agentRuntime: "openclaw",
20050
+ hookType: "PreToolUse",
20051
+ toolName: event.toolName,
20052
+ sessionId,
20053
+ result,
20054
+ ...data
20055
+ });
20056
+ };
20057
+ logger.debug("OpenClaw tool hook started", {
20058
+ agentRuntime: "openclaw",
20059
+ hookType: "PreToolUse",
20060
+ toolName: event.toolName,
20061
+ sessionId
20062
+ });
19695
20063
  try {
19696
20064
  const { toolName, params } = event;
19697
20065
  const artifacts = mapToolToArtifacts(toolName, params);
19698
- if (!artifacts || artifacts.length === 0) return void 0;
19699
- const sessionId = ctx?.sessionKey ?? "unknown";
20066
+ if (!artifacts || artifacts.length === 0) {
20067
+ completeHook("skipped", { skippedReason: "no_artifacts" });
20068
+ return void 0;
20069
+ }
20070
+ const canonicalToolName = canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName);
19700
20071
  const { verdict, actionId } = await guardToolCall(
19701
20072
  {
19702
20073
  sessionId,
19703
20074
  conversationId: sessionId,
19704
20075
  agentRuntime: "openclaw",
19705
- toolName: canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName),
20076
+ toolName: canonicalToolName,
19706
20077
  toolInput: normalizeToolInput(toolName, params),
19707
20078
  artifacts
19708
20079
  },
19709
- { threatsDir, allowlistsDir, logger },
20080
+ { threatsDir, trustedDomainsDir, logger },
19710
20081
  approvalStore
19711
20082
  );
19712
- if (verdict.decision === "allow") return void 0;
20083
+ if (verdict.decision === "allow") {
20084
+ completeHook("evaluated", {
20085
+ toolName: canonicalToolName,
20086
+ decision: verdict.decision,
20087
+ category: verdict.category,
20088
+ severity: verdict.severity,
20089
+ artifactsCount: artifacts.length
20090
+ });
20091
+ return void 0;
20092
+ }
19713
20093
  if (verdict.decision === "deny") {
20094
+ completeHook("evaluated", {
20095
+ toolName: canonicalToolName,
20096
+ decision: verdict.decision,
20097
+ category: verdict.category,
20098
+ severity: verdict.severity,
20099
+ artifactsCount: artifacts.length
20100
+ });
19714
20101
  return { block: true, blockReason: formatDenyMessage(verdict, branding2) };
19715
20102
  }
19716
- 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;
19717
20105
  const intersected = artifacts.filter((a) => verdict.artifacts.includes(a.value));
19718
20106
  const flaggedArtifacts = intersected.length > 0 ? intersected : artifacts;
20107
+ completeHook("evaluated", {
20108
+ toolName: canonicalToolName,
20109
+ decision: verdict.decision,
20110
+ category: verdict.category,
20111
+ severity: verdict.severity,
20112
+ artifactsCount: artifacts.length,
20113
+ actionId
20114
+ });
19719
20115
  return {
19720
20116
  requireApproval: {
19721
20117
  id: actionId,
@@ -19729,7 +20125,14 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
19729
20125
  timeoutBehavior: "deny",
19730
20126
  onResolution: async (decision) => {
19731
20127
  approvalStore.deletePending(actionId);
19732
- if (decision !== "allow-always") return;
20128
+ if (decision !== "allow-always") {
20129
+ logger.debug("OpenClaw approval resolved without persistent allow", {
20130
+ actionId,
20131
+ decision
20132
+ });
20133
+ return;
20134
+ }
20135
+ logger.info("OpenClaw approval accepted permanently", { actionId });
19733
20136
  for (const a of flaggedArtifacts) {
19734
20137
  await addException(
19735
20138
  { type: a.type, value: a.value },
@@ -19743,6 +20146,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
19743
20146
  };
19744
20147
  } catch (e) {
19745
20148
  logger.error("tool-handler error, failing open", { error: String(e) });
20149
+ completeHook("failed_open", { decision: "allow" });
19746
20150
  return void 0;
19747
20151
  }
19748
20152
  }
@@ -19760,34 +20164,50 @@ var index_default = {
19760
20164
  jsonSchema: { type: "object", additionalProperties: false, properties: {} }
19761
20165
  },
19762
20166
  register(api) {
19763
- const logger = createLogger(api.logger);
20167
+ const operationalLogger = createOperationalLogger(config.operational_logging, "openclaw");
20168
+ const logger = operationalLogger.forComponent("plugin");
20169
+ const toolLogger = operationalLogger.forComponent("tool-handler");
20170
+ const scanLogger = operationalLogger.forComponent("startup-scan");
19764
20171
  const approvalStore = new ApprovalStore();
19765
- const { threatsDir, allowlistsDir } = getBundledDataDirs();
20172
+ const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
19766
20173
  const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
19767
20174
  const interval = setInterval(() => approvalStore.cleanup(), CLEANUP_INTERVAL_MS);
19768
20175
  if (typeof interval.unref === "function") interval.unref();
19769
- let pendingFindings = null;
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
+ });
20186
+ let pendingScanFindings = null;
19770
20187
  const onFindings = (msg) => {
19771
- pendingFindings = msg;
20188
+ pendingScanFindings = msg;
19772
20189
  };
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
+ );
19773
20200
  api.on(
19774
20201
  "before_tool_call",
19775
- createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir, branding),
20202
+ createToolCallHandler(approvalStore, toolLogger, threatsDir, trustedDomainsDir, branding),
19776
20203
  { priority: 100 }
19777
20204
  );
19778
- api.on("gateway_start", createStartupScanHandler(logger, branding, onFindings));
19779
- api.on("session_start", createSessionScanHandler(logger, branding, onFindings));
19780
- api.on(
19781
- "before_agent_start",
19782
- createBeforeAgentStartHandler(
19783
- () => pendingFindings,
19784
- () => {
19785
- pendingFindings = null;
19786
- },
19787
- logger,
19788
- branding
19789
- )
19790
- );
20205
+ api.on("gateway_start", createStartupScanHandler(scanLogger, branding, onFindings));
20206
+ api.on("session_start", createSessionScanHandler(scanLogger, branding, onFindings));
20207
+ api.on("before_agent_start", async () => {
20208
+ await migrationCheckPromise;
20209
+ return beforeAgentStartHandler();
20210
+ });
19791
20211
  }
19792
20212
  };
19793
20213
  //# sourceMappingURL=index.cjs.map