@gendigital/sage-openclaw 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4233,7 +4233,7 @@ var init_zod = __esm({
4233
4233
  });
4234
4234
 
4235
4235
  // ../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;
4236
+ var nullLogger, ArtifactTypeSchema, ArtifactSchema, SeveritySchema, ActionSchema, ThreatSchema, DecisionSchema, VerdictSeveritySchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, AllowlistConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4237
4237
  var init_types2 = __esm({
4238
4238
  "../core/dist/types.js"() {
4239
4239
  "use strict";
@@ -4293,6 +4293,14 @@ var init_types2 = __esm({
4293
4293
  max_bytes: external_exports.number().int().min(0).default(5 * 1024 * 1024),
4294
4294
  max_files: external_exports.number().int().min(0).default(3)
4295
4295
  });
4296
+ OperationalLogLevelSchema = external_exports.enum(["debug", "info", "warn", "error"]);
4297
+ OperationalLoggingConfigSchema = external_exports.object({
4298
+ enabled: external_exports.boolean().default(true),
4299
+ level: OperationalLogLevelSchema.default("info"),
4300
+ path: external_exports.string().default("~/.sage/operational.jsonl"),
4301
+ max_bytes: external_exports.number().int().min(0).default(5 * 1024 * 1024),
4302
+ max_files: external_exports.number().int().min(0).default(3)
4303
+ });
4296
4304
  FileCheckConfigSchema = external_exports.object({
4297
4305
  endpoint: external_exports.string().optional(),
4298
4306
  timeout_seconds: external_exports.number().default(5),
@@ -4342,6 +4350,7 @@ var init_types2 = __esm({
4342
4350
  allowlist: AllowlistConfigSchema.default({}),
4343
4351
  exceptions: ExceptionsConfigSchema.default({}),
4344
4352
  logging: LoggingConfigSchema.default({}),
4353
+ operational_logging: OperationalLoggingConfigSchema.default({}),
4345
4354
  sensitivity: SensitivitySchema.default("balanced"),
4346
4355
  disabled_threats: external_exports.array(external_exports.string()).default([]),
4347
4356
  brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
@@ -4363,6 +4372,7 @@ var config_exports = {};
4363
4372
  __export(config_exports, {
4364
4373
  HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
4365
4374
  SAGE_DIR: () => SAGE_DIR,
4375
+ defaultConfigPath: () => defaultConfigPath,
4366
4376
  loadConfig: () => loadConfig,
4367
4377
  loadConfigSync: () => loadConfigSync,
4368
4378
  resolvePath: () => resolvePath
@@ -4385,6 +4395,9 @@ function defaultExceptionsPath() {
4385
4395
  function defaultAuditPath() {
4386
4396
  return (0, import_node_path2.join)(resolvedSageDir(), "audit.jsonl");
4387
4397
  }
4398
+ function defaultOperationalLogPath() {
4399
+ return (0, import_node_path2.join)(resolvedSageDir(), "operational.jsonl");
4400
+ }
4388
4401
  function resolvePath(pathStr) {
4389
4402
  if (pathStr.startsWith("~/") || pathStr === "~") {
4390
4403
  const home = getHomeDir();
@@ -4444,6 +4457,7 @@ function sanitizeConfigPaths(config2, logger) {
4444
4457
  const allowlistPath = defaultAllowlistPath();
4445
4458
  const exceptionsPath = defaultExceptionsPath();
4446
4459
  const auditPath = defaultAuditPath();
4460
+ const operationalLogPath = defaultOperationalLogPath();
4447
4461
  return {
4448
4462
  ...config2,
4449
4463
  cache: {
@@ -4461,61 +4475,50 @@ function sanitizeConfigPaths(config2, logger) {
4461
4475
  logging: {
4462
4476
  ...config2.logging,
4463
4477
  path: normalizeStateFilePath(config2.logging.path, auditPath, "logging", logger)
4478
+ },
4479
+ operational_logging: {
4480
+ ...config2.operational_logging,
4481
+ path: normalizeStateFilePath(config2.operational_logging.path, operationalLogPath, "operational_logging", logger)
4464
4482
  }
4465
4483
  };
4466
4484
  }
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
- }
4485
+ function defaultConfig(logger) {
4486
+ return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4487
+ }
4488
+ function parseConfig(raw, path, logger) {
4475
4489
  let data;
4476
4490
  try {
4477
4491
  data = JSON.parse(raw);
4478
4492
  } catch (e) {
4479
4493
  logger.warn(`Failed to parse config from ${path}`, { error: String(e) });
4480
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4494
+ return defaultConfig(logger);
4481
4495
  }
4482
4496
  if (typeof data !== "object" || data === null || Array.isArray(data)) {
4483
4497
  logger.warn(`Config file ${path} does not contain a JSON object`);
4484
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4498
+ return defaultConfig(logger);
4485
4499
  }
4486
4500
  const sanitized = sanitizeBrandKey(data, logger);
4487
4501
  try {
4488
4502
  return sanitizeConfigPaths(ConfigSchema.parse(sanitized), logger);
4489
4503
  } catch (e) {
4490
4504
  logger.warn(`Config validation failed, using defaults`, { error: String(e) });
4491
- return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4505
+ return defaultConfig(logger);
4492
4506
  }
4493
4507
  }
4494
- function loadConfigSync(configPath, logger = nullLogger) {
4495
- const path = configPath ?? defaultConfigPath();
4496
- let raw;
4508
+ async function loadConfig(configPath, logger = nullLogger) {
4509
+ const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4497
4510
  try {
4498
- raw = getFileContentSync(path);
4511
+ return parseConfig(await getFileContent(path), path, logger);
4499
4512
  } 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);
4513
+ return defaultConfig(logger);
4508
4514
  }
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);
4512
- }
4513
- const sanitized = sanitizeBrandKey(data, logger);
4515
+ }
4516
+ function loadConfigSync(configPath, logger = nullLogger) {
4517
+ const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4514
4518
  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);
4519
+ return parseConfig(getFileContentSync(path), path, logger);
4520
+ } catch {
4521
+ return defaultConfig(logger);
4519
4522
  }
4520
4523
  }
4521
4524
  var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, BRAND_KEY_RE;
@@ -14229,7 +14232,7 @@ var ApprovalStore = class _ApprovalStore {
14229
14232
  approved = /* @__PURE__ */ new Map();
14230
14233
  static actionId(toolName, params, sessionId) {
14231
14234
  const payload = JSON.stringify({ toolName, params, sessionId });
14232
- return (0, import_node_crypto3.createHash)("sha256").update(payload).digest("hex");
14235
+ return (0, import_node_crypto3.createHash)("sha256").update(payload).digest("hex").slice(0, 32);
14233
14236
  }
14234
14237
  static artifactId(type, value) {
14235
14238
  return `${type}:${value}`;
@@ -14237,6 +14240,9 @@ var ApprovalStore = class _ApprovalStore {
14237
14240
  setPending(actionId, entry) {
14238
14241
  this.pending.set(actionId, { ...entry, artifacts: [...entry.artifacts] });
14239
14242
  }
14243
+ getPending(actionId) {
14244
+ return this.pending.get(actionId) ?? null;
14245
+ }
14240
14246
  deletePending(actionId) {
14241
14247
  this.pending.delete(actionId);
14242
14248
  }
@@ -14313,59 +14319,124 @@ var ApprovalStore = class _ApprovalStore {
14313
14319
 
14314
14320
  // ../core/dist/audit-log.js
14315
14321
  var import_node_crypto4 = require("node:crypto");
14322
+ init_config();
14323
+ init_file_utils();
14324
+
14325
+ // ../core/dist/jsonl-log-writer.js
14316
14326
  var import_promises = require("node:fs/promises");
14317
14327
  var import_node_path4 = require("node:path");
14328
+ var import_promises2 = require("node:timers/promises");
14318
14329
  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) {
14330
+ var writeQueues = /* @__PURE__ */ new Map();
14331
+ var ROTATE_LOCK_TIMEOUT_MS = 250;
14332
+ var ROTATE_LOCK_STALE_MS = 3e4;
14333
+ var ROTATE_LOCK_POLL_MS = 50;
14334
+ async function shouldRotate(filePath, maxBytes, maxFiles) {
14335
14335
  if (maxBytes <= 0 || maxFiles <= 0)
14336
- return;
14337
- let size;
14336
+ return false;
14338
14337
  try {
14339
14338
  const s = await (0, import_promises.stat)(filePath);
14340
- size = s.size;
14339
+ return s.size >= maxBytes;
14341
14340
  } catch {
14342
- return;
14341
+ return false;
14343
14342
  }
14344
- if (size < maxBytes)
14345
- return;
14343
+ }
14344
+ async function removeStaleRotateLock(lockPath) {
14346
14345
  try {
14347
- await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
14346
+ const s = await (0, import_promises.stat)(lockPath);
14347
+ if (Date.now() - s.mtimeMs < ROTATE_LOCK_STALE_MS)
14348
+ return;
14349
+ await (0, import_promises.rmdir)(lockPath);
14348
14350
  } catch {
14349
14351
  }
14350
- for (let i = maxFiles - 1; i >= 1; i--) {
14352
+ }
14353
+ async function acquireRotateLock(filePath) {
14354
+ const lockPath = `${filePath}.lock`;
14355
+ const deadline = Date.now() + ROTATE_LOCK_TIMEOUT_MS;
14356
+ while (true) {
14351
14357
  try {
14352
- await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14358
+ await (0, import_promises.mkdir)(lockPath);
14359
+ return async () => {
14360
+ try {
14361
+ await (0, import_promises.rmdir)(lockPath);
14362
+ } catch {
14363
+ }
14364
+ };
14353
14365
  } catch {
14366
+ await removeStaleRotateLock(lockPath);
14367
+ const remainingMs = deadline - Date.now();
14368
+ if (remainingMs <= 0)
14369
+ return void 0;
14370
+ await (0, import_promises2.setTimeout)(Math.min(ROTATE_LOCK_POLL_MS, remainingMs));
14354
14371
  }
14355
14372
  }
14373
+ }
14374
+ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
14375
+ if (!await shouldRotate(filePath, maxBytes, maxFiles))
14376
+ return;
14377
+ const releaseLock = await acquireRotateLock(filePath);
14378
+ if (!releaseLock)
14379
+ return;
14356
14380
  try {
14357
- await (0, import_promises.rename)(filePath, `${filePath}.1`);
14358
- } catch {
14381
+ if (!await shouldRotate(filePath, maxBytes, maxFiles))
14382
+ return;
14383
+ try {
14384
+ await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
14385
+ } catch {
14386
+ }
14387
+ for (let i = maxFiles - 1; i >= 1; i--) {
14388
+ try {
14389
+ await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14390
+ } catch {
14391
+ }
14392
+ }
14393
+ try {
14394
+ await (0, import_promises.rename)(filePath, `${filePath}.1`);
14395
+ } catch {
14396
+ }
14397
+ } finally {
14398
+ await releaseLock();
14359
14399
  }
14360
14400
  }
14361
- async function appendEntry(config2, entry) {
14362
- const path = resolvePath(config2.path);
14401
+ async function appendJsonlEntryNow(path, config2, entry) {
14363
14402
  await (0, import_promises.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
14364
14403
  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)}
14404
+ await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
14367
14405
  `);
14368
14406
  }
14407
+ async function appendJsonlEntry(config2, entry) {
14408
+ const path = resolvePath(config2.path);
14409
+ const previousWrite = writeQueues.get(path) ?? Promise.resolve();
14410
+ const nextWrite = previousWrite.catch(() => {
14411
+ }).then(() => appendJsonlEntryNow(path, config2, entry));
14412
+ writeQueues.set(path, nextWrite);
14413
+ try {
14414
+ await nextWrite;
14415
+ } finally {
14416
+ if (writeQueues.get(path) === nextWrite)
14417
+ writeQueues.delete(path);
14418
+ }
14419
+ }
14420
+
14421
+ // ../core/dist/audit-log.js
14422
+ var MAX_SUMMARY_LEN = 200;
14423
+ var AUDIT_LOG_SCHEMA_VERSION = 1;
14424
+ function toolInputSummary(toolName, toolInput) {
14425
+ if (toolName === "Bash") {
14426
+ return String(toolInput.command ?? "").slice(0, MAX_SUMMARY_LEN);
14427
+ }
14428
+ if (toolName === "WebFetch") {
14429
+ return String(toolInput.url ?? "").slice(0, MAX_SUMMARY_LEN);
14430
+ }
14431
+ if (toolName === "Write" || toolName === "Edit" || toolName === "Read" || toolName === "Delete") {
14432
+ return String(toolInput.file_path ?? "").slice(0, MAX_SUMMARY_LEN);
14433
+ }
14434
+ return JSON.stringify(toolInput).slice(0, MAX_SUMMARY_LEN);
14435
+ }
14436
+ async function appendEntry(config2, entry) {
14437
+ const stamped = { ...entry, schema_version: AUDIT_LOG_SCHEMA_VERSION };
14438
+ await appendJsonlEntry(config2, stamped);
14439
+ }
14369
14440
  async function logVerdict(config2, input) {
14370
14441
  if (!config2.enabled)
14371
14442
  return;
@@ -14873,6 +14944,7 @@ var PersistentPowershellAmsiBackend = class {
14873
14944
  logger;
14874
14945
  process = null;
14875
14946
  available = false;
14947
+ invalidResultLogged = false;
14876
14948
  stdoutBuffer = "";
14877
14949
  pendingResponse = null;
14878
14950
  constructor(logger) {
@@ -14970,10 +15042,13 @@ var PersistentPowershellAmsiBackend = class {
14970
15042
  const line = await this.waitForLine(PS_TIMEOUT);
14971
15043
  const amsiResult = parseInt(line, 10);
14972
15044
  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
- });
15045
+ if (!this.invalidResultLogged) {
15046
+ this.invalidResultLogged = true;
15047
+ this.logger.warn("AMSI: PowerShell scan returned invalid result", {
15048
+ stdout: line.slice(0, 100),
15049
+ contentName
15050
+ });
15051
+ }
14977
15052
  return null;
14978
15053
  }
14979
15054
  this.logger.debug("AMSI: PowerShell scan result", { contentName, amsiResult });
@@ -15007,6 +15082,7 @@ var PersistentPowershellAmsiBackend = class {
15007
15082
  var WslPowershellAmsiBackend = class {
15008
15083
  logger;
15009
15084
  available = false;
15085
+ invalidResultLogged = false;
15010
15086
  constructor(logger) {
15011
15087
  this.logger = logger;
15012
15088
  }
@@ -15050,10 +15126,13 @@ var WslPowershellAmsiBackend = class {
15050
15126
  clearTimeout(timer);
15051
15127
  const amsiResult = parseInt(stdout.trim(), 10);
15052
15128
  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
- });
15129
+ if (!this.invalidResultLogged) {
15130
+ this.invalidResultLogged = true;
15131
+ this.logger.warn("AMSI: PowerShell one-shot invalid result", {
15132
+ stdout: stdout.slice(0, 100),
15133
+ contentName
15134
+ });
15135
+ }
15057
15136
  resolve9(null);
15058
15137
  return;
15059
15138
  }
@@ -15212,7 +15291,7 @@ init_file_utils();
15212
15291
  var import_meta = {};
15213
15292
  function resolveVersion() {
15214
15293
  if (true)
15215
- return "0.9.0";
15294
+ return "0.10.0";
15216
15295
  try {
15217
15296
  const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15218
15297
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -15392,9 +15471,6 @@ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
15392
15471
  const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
15393
15472
  return required.filter((name) => !isModelPresent(name, schema, sageDir));
15394
15473
  }
15395
- function anyRequiredModelMissing(schema = MODEL_SCHEMA_VERSION, sageDir) {
15396
- return missingRequiredModels(schema, sageDir).length > 0;
15397
- }
15398
15474
 
15399
15475
  // ../core/dist/clients/model-downloader.js
15400
15476
  init_types2();
@@ -15763,9 +15839,11 @@ var BundledPiProvider = class _BundledPiProvider {
15763
15839
  * crash (exit code 134/SIGABRT). Only exits if the runtime was actually
15764
15840
  * loaded.
15765
15841
  */
15766
- static exitIfModelLoaded() {
15767
- if (_BundledPiProvider.modelRuntimeLoaded)
15768
- process.exit(0);
15842
+ static async exitIfModelLoaded(logger) {
15843
+ if (!_BundledPiProvider.modelRuntimeLoaded)
15844
+ return;
15845
+ await logger?.flush?.();
15846
+ process.exit(0);
15769
15847
  }
15770
15848
  constructor(options = {}) {
15771
15849
  this.modelPath = options.modelPath ?? getModelDir("pi-model");
@@ -16029,6 +16107,75 @@ var SkillCheckClient = class {
16029
16107
  // ../core/dist/index.js
16030
16108
  init_config();
16031
16109
 
16110
+ // ../core/dist/config-diagnostics.js
16111
+ init_config();
16112
+ init_file_utils();
16113
+ init_types2();
16114
+ function formatPath(path) {
16115
+ return path.replace(/\\/g, "/");
16116
+ }
16117
+ function configWarning(path, reason) {
16118
+ return { file: "config", path, reason };
16119
+ }
16120
+ function exceptionsWarning(path, reason) {
16121
+ return { file: "exceptions", path, reason };
16122
+ }
16123
+ function inspectConfig(raw, path) {
16124
+ let data;
16125
+ try {
16126
+ data = JSON.parse(raw);
16127
+ } catch {
16128
+ return configWarning(path, "parse");
16129
+ }
16130
+ return ConfigSchema.safeParse(data).success ? null : configWarning(path, "validation");
16131
+ }
16132
+ function inspectExceptions(raw, path) {
16133
+ let data;
16134
+ try {
16135
+ data = JSON.parse(raw);
16136
+ } catch {
16137
+ return exceptionsWarning(path, "parse");
16138
+ }
16139
+ return ExceptionsFileSchema.safeParse(data).success ? null : exceptionsWarning(path, "validation");
16140
+ }
16141
+ function getConfigurationWarningsSync(configPath, logger) {
16142
+ const warnings = [];
16143
+ const resolvedConfigPath = configPath ? resolvePath(configPath) : defaultConfigPath();
16144
+ try {
16145
+ const rawConfig = getFileContentSync(resolvedConfigPath);
16146
+ const warning = inspectConfig(rawConfig, resolvedConfigPath);
16147
+ if (warning)
16148
+ warnings.push(warning);
16149
+ } catch {
16150
+ }
16151
+ const config2 = loadConfigSync(resolvedConfigPath, logger);
16152
+ const exceptionsPath = resolvePath(config2.exceptions.path);
16153
+ try {
16154
+ const rawExceptions = getFileContentSync(exceptionsPath);
16155
+ const warning = inspectExceptions(rawExceptions, exceptionsPath);
16156
+ if (warning)
16157
+ warnings.push(warning);
16158
+ } catch {
16159
+ }
16160
+ return warnings;
16161
+ }
16162
+ function formatConfigurationWarnings(warnings, branding2) {
16163
+ if (warnings.length === 0)
16164
+ return null;
16165
+ const lines = [`\u26A0\uFE0F ${branding2.name}: configuration warning`];
16166
+ for (const warning of warnings) {
16167
+ if (warning.file === "config") {
16168
+ const reason = warning.reason === "parse" ? "is not valid JSON" : "has the wrong shape";
16169
+ lines.push(`- ${formatPath(warning.path)} ${reason}. ${branding2.short_name} is using default settings until this file is fixed.`);
16170
+ } else if (warning.reason === "parse") {
16171
+ lines.push(`- ${formatPath(warning.path)} is not valid JSON. Exception rules are not being applied until this file is fixed.`);
16172
+ } else {
16173
+ lines.push(`- ${formatPath(warning.path)} has the wrong shape. Expected ${JSON.stringify({ rules: [] })}. Exception rules are not being applied until this file is fixed.`);
16174
+ }
16175
+ }
16176
+ return lines.join("\n");
16177
+ }
16178
+
16032
16179
  // ../core/dist/content-policy.js
16033
16180
  var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
16034
16181
  ".py",
@@ -16271,7 +16418,7 @@ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {})
16271
16418
  }
16272
16419
 
16273
16420
  // ../core/dist/extended-info.js
16274
- var import_promises2 = require("node:fs/promises");
16421
+ var import_promises3 = require("node:fs/promises");
16275
16422
  var import_node_os3 = require("node:os");
16276
16423
  var import_node_path10 = require("node:path");
16277
16424
  init_file_utils();
@@ -16285,6 +16432,23 @@ var cache = /* @__PURE__ */ new Map();
16285
16432
  function isPlainObjectValue(value) {
16286
16433
  return typeof value === "object" && value !== null && !Array.isArray(value);
16287
16434
  }
16435
+ function sanitizeScalarLeaf(leafValue) {
16436
+ if (typeof leafValue === "string") {
16437
+ return safeTruncate(leafValue, EXTENDED_INFO_MAX_LEAF_CHARS);
16438
+ }
16439
+ return leafValue;
16440
+ }
16441
+ function sanitizeArrayLeaf(groupKey, leafKey, items, logger) {
16442
+ const out = [];
16443
+ for (const [index, item] of items.entries()) {
16444
+ if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
16445
+ logger.debug(`extended-info: dropped non-scalar array item '${groupKey}.${leafKey}[${index}]' (type ${typeof item})`);
16446
+ continue;
16447
+ }
16448
+ out.push(sanitizeScalarLeaf(item));
16449
+ }
16450
+ return out;
16451
+ }
16288
16452
  function sanitizeLeaves(groupKey, rawGroup, logger) {
16289
16453
  const entries = Object.entries(rawGroup);
16290
16454
  const overflow = entries.length > EXTENDED_INFO_MAX_KEYS_PER_GROUP;
@@ -16295,11 +16459,15 @@ function sanitizeLeaves(groupKey, rawGroup, logger) {
16295
16459
  const out = {};
16296
16460
  for (const [leafKey, leafValue] of retained) {
16297
16461
  if (typeof leafValue === "string") {
16298
- out[leafKey] = safeTruncate(leafValue, EXTENDED_INFO_MAX_LEAF_CHARS);
16462
+ out[leafKey] = sanitizeScalarLeaf(leafValue);
16299
16463
  continue;
16300
16464
  }
16301
16465
  if (typeof leafValue === "number" || typeof leafValue === "boolean") {
16302
- out[leafKey] = leafValue;
16466
+ out[leafKey] = sanitizeScalarLeaf(leafValue);
16467
+ continue;
16468
+ }
16469
+ if (Array.isArray(leafValue)) {
16470
+ out[leafKey] = sanitizeArrayLeaf(groupKey, leafKey, leafValue, logger);
16303
16471
  continue;
16304
16472
  }
16305
16473
  logger.debug(`extended-info: dropped non-scalar leaf '${groupKey}.${leafKey}' (type ${typeof leafValue})`);
@@ -16330,7 +16498,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
16330
16498
  async function loadExtendedInfoUncached(filePath, logger) {
16331
16499
  let size;
16332
16500
  try {
16333
- const info = await (0, import_promises2.stat)(filePath);
16501
+ const info = await (0, import_promises3.stat)(filePath);
16334
16502
  if (!info.isFile()) {
16335
16503
  logger.debug(`extended-info: not a regular file at ${filePath}`);
16336
16504
  return null;
@@ -16367,6 +16535,26 @@ async function loadExtendedInfoUncached(filePath, logger) {
16367
16535
  }
16368
16536
  return sanitizeDocument(parsed, logger);
16369
16537
  }
16538
+ function cloneExtendedInfoLeaf(leafValue) {
16539
+ return Array.isArray(leafValue) ? dedupeArrayItems(leafValue) : leafValue;
16540
+ }
16541
+ function cloneExtendedInfoGroup(groupValue) {
16542
+ return Object.fromEntries(Object.entries(groupValue).map(([leafKey, leafValue]) => [
16543
+ leafKey,
16544
+ cloneExtendedInfoLeaf(leafValue)
16545
+ ]));
16546
+ }
16547
+ function dedupeArrayItems(items) {
16548
+ const seen = /* @__PURE__ */ new Set();
16549
+ const out = [];
16550
+ for (const item of items) {
16551
+ if (seen.has(item))
16552
+ continue;
16553
+ seen.add(item);
16554
+ out.push(item);
16555
+ }
16556
+ return out;
16557
+ }
16370
16558
  function mergeExtendedInfo(envelope, extendedInfo) {
16371
16559
  const out = { ...envelope };
16372
16560
  if (!extendedInfo)
@@ -16374,7 +16562,7 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16374
16562
  for (const [groupKey, groupValue] of Object.entries(extendedInfo)) {
16375
16563
  const existing = out[groupKey];
16376
16564
  if (existing === void 0 || existing === null) {
16377
- out[groupKey] = { ...groupValue };
16565
+ out[groupKey] = cloneExtendedInfoGroup(groupValue);
16378
16566
  continue;
16379
16567
  }
16380
16568
  if (!isPlainObjectValue(existing)) {
@@ -16384,7 +16572,11 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16384
16572
  for (const [leafKey, leafValue] of Object.entries(groupValue)) {
16385
16573
  const current = merged[leafKey];
16386
16574
  if (current === void 0 || current === null) {
16387
- merged[leafKey] = leafValue;
16575
+ merged[leafKey] = cloneExtendedInfoLeaf(leafValue);
16576
+ continue;
16577
+ }
16578
+ if (Array.isArray(current) && Array.isArray(leafValue)) {
16579
+ merged[leafKey] = dedupeArrayItems([...current, ...leafValue]);
16388
16580
  }
16389
16581
  }
16390
16582
  out[groupKey] = merged;
@@ -16394,7 +16586,7 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16394
16586
 
16395
16587
  // ../core/dist/installation-id.js
16396
16588
  var import_node_crypto5 = require("node:crypto");
16397
- var import_promises3 = require("node:fs/promises");
16589
+ var import_promises4 = require("node:fs/promises");
16398
16590
  var import_node_path11 = require("node:path");
16399
16591
  init_config();
16400
16592
  init_file_utils();
@@ -16412,8 +16604,8 @@ async function getInstallationId(sageDirPath) {
16412
16604
  }
16413
16605
  try {
16414
16606
  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" });
16607
+ await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
16608
+ await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16417
16609
  return id;
16418
16610
  } catch (err) {
16419
16611
  if (err.code === "EEXIST") {
@@ -16491,10 +16683,22 @@ async function sendCommunityIqDetection(args) {
16491
16683
  signal: AbortSignal.timeout(timeoutMs)
16492
16684
  });
16493
16685
  if (!response.ok) {
16494
- logger.debug(`Detection telemetry HTTP ${response.status}`);
16686
+ logger.warn("Detection telemetry send failed", {
16687
+ eventId: args.eventId,
16688
+ status: response.status
16689
+ });
16690
+ } else {
16691
+ logger.debug("Detection telemetry sent", {
16692
+ eventId: args.eventId,
16693
+ toolName: args.toolName,
16694
+ hookType: args.hookType ?? "PreToolUse"
16695
+ });
16495
16696
  }
16496
16697
  } catch (err) {
16497
- logger.debug(`Detection telemetry send failed: ${err}`);
16698
+ logger.warn("Detection telemetry send failed", {
16699
+ eventId: args.eventId,
16700
+ error: String(err)
16701
+ });
16498
16702
  }
16499
16703
  }
16500
16704
 
@@ -16758,7 +16962,7 @@ init_config();
16758
16962
  init_file_utils();
16759
16963
 
16760
16964
  // ../core/dist/trusted-domains.js
16761
- var import_promises4 = require("node:fs/promises");
16965
+ var import_promises5 = require("node:fs/promises");
16762
16966
  var import_node_path12 = require("node:path");
16763
16967
  var import_yaml = __toESM(require_dist(), 1);
16764
16968
  init_file_utils();
@@ -16766,7 +16970,7 @@ init_types2();
16766
16970
  async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
16767
16971
  let files;
16768
16972
  try {
16769
- files = (await (0, import_promises4.readdir)(allowlistsDir)).filter((f) => f.endsWith(".yaml")).sort();
16973
+ files = (await (0, import_promises5.readdir)(allowlistsDir)).filter((f) => f.endsWith(".yaml")).sort();
16770
16974
  } catch {
16771
16975
  logger.debug("Allowlists directory does not exist", { path: allowlistsDir });
16772
16976
  return [];
@@ -16937,8 +17141,14 @@ async function addException(artifact, reason, exceptionsPath, logger = nullLogge
16937
17141
  reason
16938
17142
  };
16939
17143
  const existing = await loadExceptions(excConfig, logger);
16940
- if (existing.some((r) => r.id === rule.id))
17144
+ if (existing.some((r) => r.id === rule.id)) {
17145
+ logger.debug("Exception rule already exists", {
17146
+ ruleId: rule.id,
17147
+ decision: rule.decision,
17148
+ match: rule.match
17149
+ });
16941
17150
  return;
17151
+ }
16942
17152
  const rules = [...existing, rule];
16943
17153
  const path = resolvePath(excConfig.path);
16944
17154
  try {
@@ -16951,6 +17161,11 @@ async function addException(artifact, reason, exceptionsPath, logger = nullLogge
16951
17161
  ...r.reason !== void 0 ? { reason: r.reason } : {}
16952
17162
  }))
16953
17163
  });
17164
+ logger.info("Exception rule added", {
17165
+ ruleId: rule.id,
17166
+ decision: rule.decision,
17167
+ match: rule.match
17168
+ });
16954
17169
  } catch (e) {
16955
17170
  logger.warn("Failed to write exception rule", { error: String(e) });
16956
17171
  }
@@ -17871,7 +18086,7 @@ async function updateSessionStatus(sessionId, verdict) {
17871
18086
  }
17872
18087
 
17873
18088
  // ../core/dist/threat-loader.js
17874
- var import_promises5 = require("node:fs/promises");
18089
+ var import_promises6 = require("node:fs/promises");
17875
18090
  var import_node_path15 = require("node:path");
17876
18091
  var import_yaml2 = __toESM(require_dist(), 1);
17877
18092
  init_file_utils();
@@ -17906,7 +18121,7 @@ async function loadThreats(threatDir, logger = nullLogger) {
17906
18121
  const threats = [];
17907
18122
  let files;
17908
18123
  try {
17909
- files = (await (0, import_promises5.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
18124
+ files = (await (0, import_promises6.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
17910
18125
  } catch {
17911
18126
  logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
17912
18127
  return threats;
@@ -17979,33 +18194,6 @@ async function loadThreats(threatDir, logger = nullLogger) {
17979
18194
  return threats;
17980
18195
  }
17981
18196
 
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
18197
  // ../core/dist/evaluator.js
18010
18198
  init_types2();
18011
18199
  var AMSI_CONTENT_SNIPPET_MAX = 200;
@@ -18046,11 +18234,43 @@ function allowVerdict(source = "none") {
18046
18234
  reasons: []
18047
18235
  };
18048
18236
  }
18237
+ function shouldSkipPromptInjectionForLocalMarkdown(request) {
18238
+ if (request.toolName !== "Write" && request.toolName !== "Edit")
18239
+ return false;
18240
+ const filePath = resolveFilePath(request.toolInput) ?? "";
18241
+ const trimmed = filePath.trim();
18242
+ return /\.(?:md|mdx|markdown|mdown|mkdn)$/i.test(trimmed);
18243
+ }
18049
18244
  async function evaluateToolCall(request, context) {
18050
18245
  const logger = context.logger ?? nullLogger;
18051
18246
  const config2 = await loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
18247
+ const eventId = (0, import_node_crypto7.randomUUID)();
18248
+ logger.debug("Tool call evaluation started", {
18249
+ eventId,
18250
+ toolUseId: request.toolUseId,
18251
+ toolName: request.toolName,
18252
+ hookType: request.hookType,
18253
+ agentRuntime: request.agentRuntime,
18254
+ artifactsCount: request.artifacts.length
18255
+ });
18256
+ const logEvaluationCompleted = (verdict2) => {
18257
+ logger.debug("Tool call evaluation completed", {
18258
+ eventId,
18259
+ toolUseId: request.toolUseId,
18260
+ toolName: request.toolName,
18261
+ hookType: request.hookType,
18262
+ agentRuntime: request.agentRuntime,
18263
+ artifactsCount: request.artifacts.length,
18264
+ decision: verdict2.decision,
18265
+ source: verdict2.source,
18266
+ category: verdict2.category,
18267
+ severity: verdict2.severity
18268
+ });
18269
+ };
18052
18270
  if (request.artifacts.length === 0 && !config2.pi_check.enabled) {
18053
- return allowVerdict("no_artifacts");
18271
+ const verdict2 = allowVerdict("no_artifacts");
18272
+ logEvaluationCompleted(verdict2);
18273
+ return verdict2;
18054
18274
  }
18055
18275
  try {
18056
18276
  const exceptions = await loadExceptions(config2.exceptions, logger);
@@ -18076,10 +18296,13 @@ async function evaluateToolCall(request, context) {
18076
18296
  verdict: verdict2,
18077
18297
  conversationId: request.conversationId,
18078
18298
  agentRuntime: request.agentRuntime,
18079
- hookType: request.hookType
18299
+ hookType: request.hookType,
18300
+ eventId,
18301
+ toolUseId: request.toolUseId
18080
18302
  });
18081
18303
  } catch {
18082
18304
  }
18305
+ logEvaluationCompleted(verdict2);
18083
18306
  return verdict2;
18084
18307
  }
18085
18308
  try {
@@ -18094,8 +18317,11 @@ async function evaluateToolCall(request, context) {
18094
18317
  userOverride: true,
18095
18318
  conversationId: request.conversationId,
18096
18319
  agentRuntime: request.agentRuntime,
18097
- hookType: request.hookType
18320
+ hookType: request.hookType,
18321
+ eventId,
18322
+ toolUseId: request.toolUseId
18098
18323
  });
18324
+ logEvaluationCompleted(allowV);
18099
18325
  return allowV;
18100
18326
  }
18101
18327
  } catch {
@@ -18112,19 +18338,24 @@ async function evaluateToolCall(request, context) {
18112
18338
  userOverride: true,
18113
18339
  conversationId: request.conversationId,
18114
18340
  agentRuntime: request.agentRuntime,
18115
- hookType: request.hookType
18341
+ hookType: request.hookType,
18342
+ eventId,
18343
+ toolUseId: request.toolUseId
18116
18344
  });
18117
18345
  } catch {
18118
18346
  }
18347
+ logEvaluationCompleted(allowV);
18119
18348
  return allowV;
18120
18349
  }
18121
- } catch {
18350
+ } catch (error) {
18351
+ logger.debug("Exception/allowlist checks failed open", { error: String(error) });
18122
18352
  }
18123
18353
  let cache2 = null;
18124
18354
  try {
18125
18355
  cache2 = new VerdictCache(config2.cache, logger, VERSION);
18126
18356
  await cache2.load();
18127
- } catch {
18357
+ } catch (error) {
18358
+ logger.debug("Verdict cache initialization failed open", { error: String(error) });
18128
18359
  cache2 = null;
18129
18360
  }
18130
18361
  const urls = request.artifacts.filter((artifact) => artifact.type === "url").map((artifact) => artifact.value);
@@ -18136,6 +18367,9 @@ async function evaluateToolCall(request, context) {
18136
18367
  const disabledSet = new Set(config2.disabled_threats);
18137
18368
  threats = threats.filter((t) => !disabledSet.has(t.id));
18138
18369
  }
18370
+ if (shouldSkipPromptInjectionForLocalMarkdown(request)) {
18371
+ threats = threats.filter((t) => t.category !== "prompt_injection");
18372
+ }
18139
18373
  const trustedDomains = await loadTrustedDomains(context.allowlistsDir, logger);
18140
18374
  const heuristics = new HeuristicsEngine(threats, trustedDomains);
18141
18375
  heuristicMatches = heuristics.match(request.artifacts);
@@ -18339,7 +18573,6 @@ async function evaluateToolCall(request, context) {
18339
18573
  auditSignals.amsi_checks = relevantAmsi.map((r) => buildAmsiSignal(r));
18340
18574
  }
18341
18575
  }
18342
- const eventId = (0, import_node_crypto7.randomUUID)();
18343
18576
  const resolvedSignals = Object.keys(auditSignals).length > 0 ? auditSignals : void 0;
18344
18577
  const builtContent = buildContentSnapshot(request.toolName, request.toolInput, request.artifacts, auditSignals);
18345
18578
  const resolvedContent = Object.keys(builtContent).length > 0 ? builtContent : void 0;
@@ -18357,7 +18590,8 @@ async function evaluateToolCall(request, context) {
18357
18590
  eventId,
18358
18591
  toolUseId: request.toolUseId
18359
18592
  });
18360
- } catch {
18593
+ } catch (error) {
18594
+ logger.debug("Audit verdict logging failed open", { error: String(error) });
18361
18595
  }
18362
18596
  if (verdict.decision === "deny") {
18363
18597
  try {
@@ -18372,18 +18606,21 @@ async function evaluateToolCall(request, context) {
18372
18606
  communityIqEnabled: config2.community_iq,
18373
18607
  logger
18374
18608
  });
18375
- } catch {
18609
+ } catch (error) {
18610
+ logger.debug("Detection telemetry failed open", { error: String(error) });
18376
18611
  }
18377
18612
  }
18378
18613
  if (verdict.decision !== "allow") {
18379
18614
  try {
18380
18615
  await updateSessionStatus(request.sessionId, verdict);
18381
- } catch {
18616
+ } catch (error) {
18617
+ logger.debug("Session status update failed open", { error: String(error) });
18382
18618
  }
18383
18619
  }
18384
18620
  const piWarnings = allPiResults.filter((r) => r.risk >= config2.pi_check.medium_risk_threshold && r.risk < config2.pi_check.high_risk_threshold);
18385
18621
  if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
18386
18622
  verdict.piWarnings = piWarnings;
18623
+ logEvaluationCompleted(verdict);
18387
18624
  return verdict;
18388
18625
  }
18389
18626
  function partitionUrlsByCache(urls, cache2) {
@@ -18683,9 +18920,125 @@ init_config();
18683
18920
  // ../core/dist/model-download.js
18684
18921
  init_types2();
18685
18922
 
18923
+ // ../core/dist/operational-log.js
18924
+ var LEVEL_PRIORITY = {
18925
+ debug: 10,
18926
+ info: 20,
18927
+ warn: 30,
18928
+ error: 40
18929
+ };
18930
+ var FLUSH_TIMEOUT_MS = 250;
18931
+ function shouldLog(config2, level) {
18932
+ return config2.enabled && LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[config2.level];
18933
+ }
18934
+ function normalizeData(data) {
18935
+ if (!data)
18936
+ return void 0;
18937
+ const seen = /* @__PURE__ */ new WeakSet();
18938
+ try {
18939
+ return JSON.parse(JSON.stringify(data, (_key, value) => {
18940
+ if (typeof value === "bigint")
18941
+ return value.toString();
18942
+ if (value instanceof Error) {
18943
+ return { name: value.name, message: value.message, stack: value.stack };
18944
+ }
18945
+ if (value && typeof value === "object") {
18946
+ if (seen.has(value))
18947
+ return "[Circular]";
18948
+ seen.add(value);
18949
+ }
18950
+ return value;
18951
+ }));
18952
+ } catch {
18953
+ return { serialization_error: "Failed to serialize log data" };
18954
+ }
18955
+ }
18956
+ async function writeOperationalLogEntry(config2, entry) {
18957
+ if (!shouldLog(config2, entry.level))
18958
+ return;
18959
+ try {
18960
+ await appendJsonlEntry(config2, entry);
18961
+ } catch {
18962
+ }
18963
+ }
18964
+ function createOperationalLogger(config2, runtime) {
18965
+ const pendingWrites = /* @__PURE__ */ new Set();
18966
+ function log(component, level, message, data) {
18967
+ if (!shouldLog(config2, level))
18968
+ return;
18969
+ const entry = {
18970
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18971
+ level,
18972
+ runtime,
18973
+ component,
18974
+ message,
18975
+ ...data ? { data: normalizeData(data) } : {}
18976
+ };
18977
+ const write = writeOperationalLogEntry(config2, entry);
18978
+ pendingWrites.add(write);
18979
+ void write.finally(() => pendingWrites.delete(write));
18980
+ }
18981
+ async function flush() {
18982
+ const deadline = Date.now() + FLUSH_TIMEOUT_MS;
18983
+ while (pendingWrites.size > 0) {
18984
+ const remainingMs = deadline - Date.now();
18985
+ if (remainingMs <= 0)
18986
+ return;
18987
+ let timeout;
18988
+ try {
18989
+ const result = await Promise.race([
18990
+ Promise.allSettled([...pendingWrites]).then(() => "drained"),
18991
+ new Promise((resolve9) => {
18992
+ timeout = setTimeout(() => resolve9("timeout"), remainingMs);
18993
+ timeout.unref?.();
18994
+ })
18995
+ ]);
18996
+ if (result === "timeout")
18997
+ return;
18998
+ } finally {
18999
+ if (timeout)
19000
+ clearTimeout(timeout);
19001
+ }
19002
+ }
19003
+ }
19004
+ function forComponent(component) {
19005
+ return {
19006
+ debug(msg, data) {
19007
+ log(component, "debug", msg, data);
19008
+ },
19009
+ info(msg, data) {
19010
+ log(component, "info", msg, data);
19011
+ },
19012
+ warn(msg, data) {
19013
+ log(component, "warn", msg, data);
19014
+ },
19015
+ error(msg, data) {
19016
+ log(component, "error", msg, data);
19017
+ },
19018
+ flush
19019
+ };
19020
+ }
19021
+ return {
19022
+ debug(component, msg, data) {
19023
+ log(component, "debug", msg, data);
19024
+ },
19025
+ info(component, msg, data) {
19026
+ log(component, "info", msg, data);
19027
+ },
19028
+ warn(component, msg, data) {
19029
+ log(component, "warn", msg, data);
19030
+ },
19031
+ error(component, msg, data) {
19032
+ log(component, "error", msg, data);
19033
+ },
19034
+ forComponent,
19035
+ flush
19036
+ };
19037
+ }
19038
+
18686
19039
  // ../core/dist/plugin-scan-cache.js
18687
19040
  var import_node_crypto8 = require("node:crypto");
18688
- var import_promises6 = require("node:fs/promises");
19041
+ var import_promises7 = require("node:fs/promises");
18689
19042
  var import_node_path16 = require("node:path");
18690
19043
  init_file_utils();
18691
19044
  init_types2();
@@ -18701,7 +19054,7 @@ async function computeConfigHash(sageVersion, ...dirs) {
18701
19054
  for (const dir of dirs) {
18702
19055
  let files;
18703
19056
  try {
18704
- files = (await (0, import_promises6.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
19057
+ files = (await (0, import_promises7.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
18705
19058
  } catch {
18706
19059
  continue;
18707
19060
  }
@@ -18726,7 +19079,7 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
18726
19079
  const data = JSON.parse(raw);
18727
19080
  const storedHash = data.config_hash ?? "";
18728
19081
  if (configHash && storedHash !== configHash) {
18729
- logger.info("Config hash changed, invalidating plugin scan cache", {
19082
+ logger.debug("Config hash changed, invalidating plugin scan cache", {
18730
19083
  from: storedHash,
18731
19084
  to: configHash
18732
19085
  });
@@ -18798,13 +19151,13 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
18798
19151
 
18799
19152
  // ../core/dist/plugin-scanner.js
18800
19153
  var import_node_crypto10 = require("node:crypto");
18801
- var import_promises8 = require("node:fs/promises");
19154
+ var import_promises9 = require("node:fs/promises");
18802
19155
  var import_node_path18 = require("node:path");
18803
19156
  init_file_utils();
18804
19157
 
18805
19158
  // ../core/dist/skill-id.js
18806
19159
  var import_node_crypto9 = require("node:crypto");
18807
- var import_promises7 = require("node:fs/promises");
19160
+ var import_promises8 = require("node:fs/promises");
18808
19161
  var import_node_path17 = require("node:path");
18809
19162
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
18810
19163
  function isContained(childReal, rootReal) {
@@ -18820,22 +19173,22 @@ function normalizePath(p) {
18820
19173
  }
18821
19174
  async function entriesFromDirectory(dirPath) {
18822
19175
  const absDir = (0, import_node_path17.resolve)(dirPath);
18823
- const rootReal = await (0, import_promises7.realpath)(absDir);
19176
+ const rootReal = await (0, import_promises8.realpath)(absDir);
18824
19177
  const entries = [];
18825
19178
  const visited = /* @__PURE__ */ new Set();
18826
19179
  async function walk(currentPath) {
18827
- const real = await (0, import_promises7.realpath)(currentPath);
19180
+ const real = await (0, import_promises8.realpath)(currentPath);
18828
19181
  if (visited.has(real))
18829
19182
  return;
18830
19183
  if (!isContained(real, rootReal))
18831
19184
  return;
18832
19185
  visited.add(real);
18833
- const items = await (0, import_promises7.readdir)(currentPath);
19186
+ const items = await (0, import_promises8.readdir)(currentPath);
18834
19187
  const dirs = [];
18835
19188
  const files = [];
18836
19189
  for (const item of items) {
18837
19190
  const fullPath = (0, import_node_path17.join)(currentPath, item);
18838
- const st = await (0, import_promises7.stat)(fullPath);
19191
+ const st = await (0, import_promises8.stat)(fullPath);
18839
19192
  if (st.isDirectory())
18840
19193
  dirs.push(item);
18841
19194
  else if (st.isFile())
@@ -18851,14 +19204,14 @@ async function entriesFromDirectory(dirPath) {
18851
19204
  }
18852
19205
  for (const f of files) {
18853
19206
  const fullPath = (0, import_node_path17.join)(currentPath, f);
18854
- const fileReal = await (0, import_promises7.realpath)(fullPath);
19207
+ const fileReal = await (0, import_promises8.realpath)(fullPath);
18855
19208
  if (!isContained(fileReal, rootReal))
18856
19209
  continue;
18857
19210
  const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
18858
19211
  entries.push({
18859
19212
  entryPath: relPath,
18860
19213
  isDir: false,
18861
- content: await (0, import_promises7.readFile)(fullPath)
19214
+ content: await (0, import_promises8.readFile)(fullPath)
18862
19215
  });
18863
19216
  }
18864
19217
  }
@@ -18899,10 +19252,10 @@ async function findSkillPackages(rootDir) {
18899
19252
  const found = [];
18900
19253
  let rootReal;
18901
19254
  try {
18902
- const rootStat = await (0, import_promises7.stat)(rootDir);
19255
+ const rootStat = await (0, import_promises8.stat)(rootDir);
18903
19256
  if (!rootStat.isDirectory())
18904
19257
  return found;
18905
- rootReal = await (0, import_promises7.realpath)((0, import_node_path17.resolve)(rootDir));
19258
+ rootReal = await (0, import_promises8.realpath)((0, import_node_path17.resolve)(rootDir));
18906
19259
  } catch {
18907
19260
  return found;
18908
19261
  }
@@ -18910,7 +19263,7 @@ async function findSkillPackages(rootDir) {
18910
19263
  async function walk(dir) {
18911
19264
  let real;
18912
19265
  try {
18913
- real = await (0, import_promises7.realpath)(dir);
19266
+ real = await (0, import_promises8.realpath)(dir);
18914
19267
  } catch {
18915
19268
  return;
18916
19269
  }
@@ -18921,14 +19274,14 @@ async function findSkillPackages(rootDir) {
18921
19274
  visited.add(real);
18922
19275
  let items;
18923
19276
  try {
18924
- items = await (0, import_promises7.readdir)(dir);
19277
+ items = await (0, import_promises8.readdir)(dir);
18925
19278
  } catch {
18926
19279
  return;
18927
19280
  }
18928
19281
  items.sort();
18929
19282
  if (items.includes("SKILL.md")) {
18930
19283
  try {
18931
- const st = await (0, import_promises7.stat)((0, import_node_path17.join)(dir, "SKILL.md"));
19284
+ const st = await (0, import_promises8.stat)((0, import_node_path17.join)(dir, "SKILL.md"));
18932
19285
  if (st.isFile())
18933
19286
  found.push(dir);
18934
19287
  } catch {
@@ -18940,7 +19293,7 @@ async function findSkillPackages(rootDir) {
18940
19293
  const fullPath = (0, import_node_path17.join)(dir, item);
18941
19294
  let st;
18942
19295
  try {
18943
- st = await (0, import_promises7.stat)(fullPath);
19296
+ st = await (0, import_promises8.stat)(fullPath);
18944
19297
  } catch {
18945
19298
  continue;
18946
19299
  }
@@ -18994,7 +19347,7 @@ async function walkPluginFiles(installPath, logger) {
18994
19347
  async function walk(dirOrFile) {
18995
19348
  let stats;
18996
19349
  try {
18997
- stats = await (0, import_promises8.stat)(dirOrFile);
19350
+ stats = await (0, import_promises9.stat)(dirOrFile);
18998
19351
  } catch {
18999
19352
  return;
19000
19353
  }
@@ -19007,7 +19360,7 @@ async function walkPluginFiles(installPath, logger) {
19007
19360
  if (stats.isDirectory()) {
19008
19361
  let entries;
19009
19362
  try {
19010
- entries = await (0, import_promises8.readdir)(dirOrFile);
19363
+ entries = await (0, import_promises9.readdir)(dirOrFile);
19011
19364
  } catch {
19012
19365
  return;
19013
19366
  }
@@ -19200,6 +19553,16 @@ async function runSessionStartScan(context) {
19200
19553
  const sageConfig = await loadConfig(context.configPath, logger);
19201
19554
  const plugins = context.plugins;
19202
19555
  if (plugins.length === 0) {
19556
+ logger.debug("Session plugin scan completed", {
19557
+ totalPlugins: 0,
19558
+ resultsWithFindings: 0,
19559
+ findingsCount: 0,
19560
+ cacheHits: 0,
19561
+ cacheMisses: 0,
19562
+ scanned: 0,
19563
+ allowedByException: 0,
19564
+ deniedByException: 0
19565
+ });
19203
19566
  return [];
19204
19567
  }
19205
19568
  let exceptions = [];
@@ -19232,9 +19595,21 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19232
19595
  const cache2 = await loadScanCache(configHash, context.scanCachePath, logger);
19233
19596
  const resultsWithFindings = [];
19234
19597
  let cacheModified = false;
19598
+ const stats = {
19599
+ totalPlugins: plugins.length,
19600
+ cacheHitsClean: 0,
19601
+ cacheHitsWithFindings: 0,
19602
+ cacheMisses: 0,
19603
+ scanned: 0,
19604
+ allowedByException: 0,
19605
+ deniedByException: 0,
19606
+ findingsCount: 0
19607
+ };
19235
19608
  for (const plugin of plugins) {
19236
19609
  const denyMatch = findPluginDenyException(exceptions, plugin.key);
19237
19610
  if (denyMatch) {
19611
+ stats.deniedByException += 1;
19612
+ stats.findingsCount += 1;
19238
19613
  resultsWithFindings.push({
19239
19614
  plugin,
19240
19615
  findings: [
@@ -19253,19 +19628,25 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19253
19628
  }
19254
19629
  const allowMatch = findPluginAllowException(exceptions, plugin.key);
19255
19630
  if (allowMatch) {
19631
+ stats.allowedByException += 1;
19256
19632
  continue;
19257
19633
  }
19258
19634
  const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
19259
19635
  if (cached && cached.findings.length === 0) {
19636
+ stats.cacheHitsClean += 1;
19260
19637
  continue;
19261
19638
  }
19262
19639
  if (cached && cached.findings.length > 0) {
19640
+ stats.cacheHitsWithFindings += 1;
19641
+ stats.findingsCount += cached.findings.length;
19263
19642
  resultsWithFindings.push({
19264
19643
  plugin,
19265
19644
  findings: cached.findings.map(fromCachedFinding)
19266
19645
  });
19267
19646
  continue;
19268
19647
  }
19648
+ stats.cacheMisses += 1;
19649
+ stats.scanned += 1;
19269
19650
  const result = await scanPlugin(plugin, {
19270
19651
  checkUrls: context.checkUrls ?? true,
19271
19652
  checkFileHashes: context.checkFileHashes ?? true,
@@ -19275,6 +19656,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19275
19656
  storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
19276
19657
  cacheModified = true;
19277
19658
  if (result.findings.length > 0) {
19659
+ stats.findingsCount += result.findings.length;
19278
19660
  resultsWithFindings.push(result);
19279
19661
  }
19280
19662
  }
@@ -19287,6 +19669,11 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19287
19669
  }
19288
19670
  } catch {
19289
19671
  }
19672
+ logger.debug("Session plugin scan completed", {
19673
+ ...stats,
19674
+ cacheHits: stats.cacheHitsClean + stats.cacheHitsWithFindings,
19675
+ resultsWithFindings: resultsWithFindings.length
19676
+ });
19290
19677
  return resultsWithFindings;
19291
19678
  }
19292
19679
 
@@ -19401,14 +19788,30 @@ async function runSessionStart(ctx) {
19401
19788
  return { scanResults, versionCheck };
19402
19789
  }
19403
19790
  async function maybeSpawnModelDownloadWorker(args) {
19404
- if (!args.workerPath)
19791
+ if (!args.workerPath) {
19792
+ args.logger.debug("Model download worker skipped", {
19793
+ result: "skipped",
19794
+ skippedReason: "missing_worker_path"
19795
+ });
19405
19796
  return;
19797
+ }
19406
19798
  if (!(0, import_node_fs4.existsSync)(args.workerPath)) {
19407
- args.logger.debug(`model download worker script not found at ${args.workerPath}`);
19799
+ args.logger.debug("Model download worker skipped", {
19800
+ result: "skipped",
19801
+ skippedReason: "worker_script_not_found",
19802
+ workerPath: args.workerPath
19803
+ });
19408
19804
  return;
19409
19805
  }
19410
- if (!anyRequiredModelMissing(MODEL_SCHEMA_VERSION, args.sageDirPath))
19806
+ const missingModels = missingRequiredModels(MODEL_SCHEMA_VERSION, args.sageDirPath);
19807
+ if (missingModels.length === 0) {
19808
+ args.logger.debug("Model download worker skipped", {
19809
+ result: "skipped",
19810
+ skippedReason: "no_missing_models",
19811
+ schema: MODEL_SCHEMA_VERSION
19812
+ });
19411
19813
  return;
19814
+ }
19412
19815
  let piEnabled = false;
19413
19816
  try {
19414
19817
  const config2 = await loadConfig(args.configPath, args.logger);
@@ -19416,9 +19819,16 @@ async function maybeSpawnModelDownloadWorker(args) {
19416
19819
  } catch {
19417
19820
  piEnabled = false;
19418
19821
  }
19419
- if (!piEnabled)
19822
+ if (!piEnabled) {
19823
+ args.logger.debug("Model download worker skipped", {
19824
+ result: "skipped",
19825
+ skippedReason: "pi_check_disabled",
19826
+ schema: MODEL_SCHEMA_VERSION,
19827
+ missingModels
19828
+ });
19420
19829
  return;
19421
- spawnModelDownloadWorker({
19830
+ }
19831
+ const spawned = spawnModelDownloadWorker({
19422
19832
  sageDirPath: args.sageDirPath,
19423
19833
  workerPath: args.workerPath,
19424
19834
  agentRuntime: args.agentRuntime,
@@ -19426,6 +19836,11 @@ async function maybeSpawnModelDownloadWorker(args) {
19426
19836
  versionApp: args.versionApp,
19427
19837
  logger: args.logger
19428
19838
  });
19839
+ args.logger.debug("Model download worker spawn completed", {
19840
+ result: spawned ? "spawned" : "failed",
19841
+ schema: MODEL_SCHEMA_VERSION,
19842
+ missingModels
19843
+ });
19429
19844
  }
19430
19845
  function spawnModelDownloadWorker(args) {
19431
19846
  const logger = args.logger ?? nullLogger;
@@ -19443,14 +19858,18 @@ function spawnModelDownloadWorker(args) {
19443
19858
  }
19444
19859
  });
19445
19860
  child.unref();
19861
+ return true;
19446
19862
  } catch (err) {
19447
- logger.debug(`failed to spawn model download worker: ${err}`);
19863
+ logger.warn("Failed to spawn model download worker", { error: String(err) });
19864
+ return false;
19448
19865
  }
19449
19866
  }
19450
19867
 
19451
19868
  // ../core/dist/scan-handler.js
19452
19869
  async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose") {
19453
- logger.info(`${branding2.name} plugin scan started (${context})`, {
19870
+ logger.debug(`${branding2.name} plugin scan started (${context})`, {
19871
+ agentRuntime,
19872
+ pluginsCount: plugins.length,
19454
19873
  threatsDir,
19455
19874
  allowlistsDir
19456
19875
  });
@@ -19463,10 +19882,19 @@ async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir
19463
19882
  agentRuntime,
19464
19883
  modelDownloadWorkerPath
19465
19884
  });
19466
- logger.info(`${branding2.name} plugin scan (${context}) complete`, {
19467
- findings: result.scanResults.length,
19885
+ const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
19886
+ const completionData = {
19887
+ agentRuntime,
19888
+ pluginsCount: plugins.length,
19889
+ resultsWithFindings: result.scanResults.length,
19890
+ findingsCount,
19468
19891
  updateAvailable: result.versionCheck?.updateAvailable ?? false
19469
- });
19892
+ };
19893
+ if (findingsCount > 0) {
19894
+ logger.warn(`${branding2.name} plugin scan (${context}) complete with findings`, completionData);
19895
+ } else {
19896
+ logger.debug(`${branding2.name} plugin scan (${context}) complete`, completionData);
19897
+ }
19470
19898
  return formatSessionStartMessage(version, result, branding2, style);
19471
19899
  }
19472
19900
  function createScanHandler(options) {
@@ -19476,7 +19904,7 @@ function createScanHandler(options) {
19476
19904
  let plugins = await discoverPlugins2();
19477
19905
  plugins = plugins.filter((p) => !p.key.startsWith(selfPrefix));
19478
19906
  if (plugins.length === 0) {
19479
- logger.warn(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
19907
+ logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
19480
19908
  }
19481
19909
  const msg = await runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style);
19482
19910
  onResult?.(msg);
@@ -19486,6 +19914,33 @@ function createScanHandler(options) {
19486
19914
  };
19487
19915
  }
19488
19916
 
19917
+ // ../core/dist/tool-names.js
19918
+ var CANONICAL_TOOLS = [
19919
+ "Bash",
19920
+ "WebFetch",
19921
+ "Write",
19922
+ "Edit",
19923
+ "Read",
19924
+ "Delete",
19925
+ "ApplyPatch",
19926
+ "Glob",
19927
+ "Grep",
19928
+ "List",
19929
+ "CodeSearch",
19930
+ "WebSearch",
19931
+ "Question",
19932
+ "Task",
19933
+ "ReadLines",
19934
+ "MCP",
19935
+ "Unknown"
19936
+ ];
19937
+ var CANONICAL_SET = new Set(CANONICAL_TOOLS);
19938
+ function canonicalizeToolName(map, rawName) {
19939
+ if (CANONICAL_SET.has(rawName))
19940
+ return rawName;
19941
+ return map[rawName] ?? "Unknown";
19942
+ }
19943
+
19489
19944
  // ../core/dist/index.js
19490
19945
  init_types2();
19491
19946
 
@@ -19510,42 +19965,21 @@ function getSageVersion() {
19510
19965
  return cachedVersion;
19511
19966
  }
19512
19967
 
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
19968
  // src/startup-scan.ts
19535
19969
  var import_node_path21 = require("node:path");
19536
19970
 
19537
19971
  // src/plugin-discovery.ts
19538
- var import_promises9 = require("node:fs/promises");
19972
+ var import_promises10 = require("node:fs/promises");
19539
19973
  var import_node_os4 = require("node:os");
19540
19974
  var import_node_path20 = require("node:path");
19541
19975
  var DEFAULT_EXTENSIONS_DIR = (0, import_node_path20.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
19542
19976
  async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding) {
19543
- logger.info(`${branding2.name} plugin discovery: scanning extensions directory`, {
19977
+ logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
19544
19978
  path: extensionsDir
19545
19979
  });
19546
19980
  let entries;
19547
19981
  try {
19548
- entries = await (0, import_promises9.readdir)(extensionsDir);
19982
+ entries = await (0, import_promises10.readdir)(extensionsDir);
19549
19983
  } catch {
19550
19984
  logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
19551
19985
  return [];
@@ -19555,7 +19989,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
19555
19989
  const extDir = (0, import_node_path20.join)(extensionsDir, entry);
19556
19990
  let stats;
19557
19991
  try {
19558
- stats = await (0, import_promises9.stat)(extDir);
19992
+ stats = await (0, import_promises10.stat)(extDir);
19559
19993
  if (!stats.isDirectory()) continue;
19560
19994
  } catch {
19561
19995
  continue;
@@ -19583,7 +20017,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
19583
20017
  lastUpdated: stats.mtime.toISOString()
19584
20018
  });
19585
20019
  }
19586
- logger.info(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
20020
+ logger.debug(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
19587
20021
  return plugins;
19588
20022
  }
19589
20023
 
@@ -19616,7 +20050,7 @@ function createBeforeAgentStartHandler(getFindings, clearFindings, logger, brand
19616
20050
  const findings = getFindings();
19617
20051
  if (!findings) return void 0;
19618
20052
  clearFindings();
19619
- logger.info(`${branding2.name}: surfacing plugin scan findings via before_agent_start`);
20053
+ logger.debug(`${branding2.name}: surfacing plugin scan findings via before_agent_start`);
19620
20054
  const prependContext = [
19621
20055
  '<security-alert source="sage-plugin-scan">',
19622
20056
  findings,
@@ -19692,30 +20126,74 @@ function mapToolToArtifacts(toolName, params) {
19692
20126
  }
19693
20127
  function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir, branding2 = defaultBranding) {
19694
20128
  async function handleToolCall(event, ctx) {
20129
+ const sessionId = ctx?.sessionKey ?? "unknown";
20130
+ const completeHook = (result, data = {}) => {
20131
+ logger.debug("OpenClaw tool hook completed", {
20132
+ agentRuntime: "openclaw",
20133
+ hookType: "PreToolUse",
20134
+ toolName: event.toolName,
20135
+ sessionId,
20136
+ result,
20137
+ ...data
20138
+ });
20139
+ };
20140
+ logger.debug("OpenClaw tool hook started", {
20141
+ agentRuntime: "openclaw",
20142
+ hookType: "PreToolUse",
20143
+ toolName: event.toolName,
20144
+ sessionId
20145
+ });
19695
20146
  try {
19696
20147
  const { toolName, params } = event;
19697
20148
  const artifacts = mapToolToArtifacts(toolName, params);
19698
- if (!artifacts || artifacts.length === 0) return void 0;
19699
- const sessionId = ctx?.sessionKey ?? "unknown";
20149
+ if (!artifacts || artifacts.length === 0) {
20150
+ completeHook("skipped", { skippedReason: "no_artifacts" });
20151
+ return void 0;
20152
+ }
20153
+ const canonicalToolName = canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName);
19700
20154
  const { verdict, actionId } = await guardToolCall(
19701
20155
  {
19702
20156
  sessionId,
19703
20157
  conversationId: sessionId,
19704
20158
  agentRuntime: "openclaw",
19705
- toolName: canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName),
20159
+ toolName: canonicalToolName,
19706
20160
  toolInput: normalizeToolInput(toolName, params),
19707
20161
  artifacts
19708
20162
  },
19709
20163
  { threatsDir, allowlistsDir, logger },
19710
20164
  approvalStore
19711
20165
  );
19712
- if (verdict.decision === "allow") return void 0;
20166
+ if (verdict.decision === "allow") {
20167
+ completeHook("evaluated", {
20168
+ toolName: canonicalToolName,
20169
+ decision: verdict.decision,
20170
+ category: verdict.category,
20171
+ severity: verdict.severity,
20172
+ artifactsCount: artifacts.length
20173
+ });
20174
+ return void 0;
20175
+ }
19713
20176
  if (verdict.decision === "deny") {
20177
+ completeHook("evaluated", {
20178
+ toolName: canonicalToolName,
20179
+ decision: verdict.decision,
20180
+ category: verdict.category,
20181
+ severity: verdict.severity,
20182
+ artifactsCount: artifacts.length
20183
+ });
19714
20184
  return { block: true, blockReason: formatDenyMessage(verdict, branding2) };
19715
20185
  }
19716
20186
  const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 3).join("; ") : verdict.category;
19717
20187
  const intersected = artifacts.filter((a) => verdict.artifacts.includes(a.value));
19718
20188
  const flaggedArtifacts = intersected.length > 0 ? intersected : artifacts;
20189
+ completeHook("evaluated", {
20190
+ toolName: canonicalToolName,
20191
+ decision: verdict.decision,
20192
+ category: verdict.category,
20193
+ severity: verdict.severity,
20194
+ artifactsCount: artifacts.length,
20195
+ actionId
20196
+ });
19719
20197
  return {
19720
20198
  requireApproval: {
19721
20199
  id: actionId,
@@ -19729,7 +20207,14 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
19729
20207
  timeoutBehavior: "deny",
19730
20208
  onResolution: async (decision) => {
19731
20209
  approvalStore.deletePending(actionId);
19732
- if (decision !== "allow-always") return;
20210
+ if (decision !== "allow-always") {
20211
+ logger.debug("OpenClaw approval resolved without persistent allow", {
20212
+ actionId,
20213
+ decision
20214
+ });
20215
+ return;
20216
+ }
20217
+ logger.info("OpenClaw approval accepted permanently", { actionId });
19733
20218
  for (const a of flaggedArtifacts) {
19734
20219
  await addException(
19735
20220
  { type: a.type, value: a.value },
@@ -19743,6 +20228,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
19743
20228
  };
19744
20229
  } catch (e) {
19745
20230
  logger.error("tool-handler error, failing open", { error: String(e) });
20231
+ completeHook("failed_open", { decision: "allow" });
19746
20232
  return void 0;
19747
20233
  }
19748
20234
  }
@@ -19760,29 +20246,35 @@ var index_default = {
19760
20246
  jsonSchema: { type: "object", additionalProperties: false, properties: {} }
19761
20247
  },
19762
20248
  register(api) {
19763
- const logger = createLogger(api.logger);
20249
+ const operationalLogger = createOperationalLogger(config.operational_logging, "openclaw");
20250
+ const logger = operationalLogger.forComponent("plugin");
20251
+ const toolLogger = operationalLogger.forComponent("tool-handler");
20252
+ const scanLogger = operationalLogger.forComponent("startup-scan");
19764
20253
  const approvalStore = new ApprovalStore();
19765
20254
  const { threatsDir, allowlistsDir } = getBundledDataDirs();
19766
20255
  const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
19767
20256
  const interval = setInterval(() => approvalStore.cleanup(), CLEANUP_INTERVAL_MS);
19768
20257
  if (typeof interval.unref === "function") interval.unref();
19769
- let pendingFindings = null;
20258
+ let pendingConfigurationWarnings = formatConfigurationWarnings(getConfigurationWarningsSync(void 0, logger), branding) ?? null;
20259
+ let pendingScanFindings = null;
19770
20260
  const onFindings = (msg) => {
19771
- pendingFindings = msg;
20261
+ pendingScanFindings = msg;
19772
20262
  };
20263
+ const getPendingFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
19773
20264
  api.on(
19774
20265
  "before_tool_call",
19775
- createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir, branding),
20266
+ createToolCallHandler(approvalStore, toolLogger, threatsDir, allowlistsDir, branding),
19776
20267
  { priority: 100 }
19777
20268
  );
19778
- api.on("gateway_start", createStartupScanHandler(logger, branding, onFindings));
19779
- api.on("session_start", createSessionScanHandler(logger, branding, onFindings));
20269
+ api.on("gateway_start", createStartupScanHandler(scanLogger, branding, onFindings));
20270
+ api.on("session_start", createSessionScanHandler(scanLogger, branding, onFindings));
19780
20271
  api.on(
19781
20272
  "before_agent_start",
19782
20273
  createBeforeAgentStartHandler(
19783
- () => pendingFindings,
20274
+ getPendingFindings,
19784
20275
  () => {
19785
- pendingFindings = null;
20276
+ pendingConfigurationWarnings = null;
20277
+ pendingScanFindings = null;
19786
20278
  },
19787
20279
  logger,
19788
20280
  branding