@gendigital/sage-openclaw 0.12.0 → 0.13.1

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/dist/index.cjs +229 -46
  2. package/dist/index.cjs.map +3 -3
  3. package/dist/mcp-server.cjs +38 -10
  4. package/dist/mcp-server.cjs.map +2 -2
  5. package/dist/model-download-worker.cjs +4 -2
  6. package/dist/model-download-worker.cjs.map +2 -2
  7. package/dist/skill-upload-worker.cjs +15 -9
  8. package/dist/skill-upload-worker.cjs.map +2 -2
  9. package/openclaw.plugin.json +1 -1
  10. package/package.json +3 -3
  11. package/resources/threats/_macros.yaml +193 -0
  12. package/resources/threats/agent-layer.yaml +19 -1
  13. package/resources/threats/commands.yaml +165 -31
  14. package/resources/threats/credentials.yaml +24 -4
  15. package/resources/threats/dummy.yaml +30 -0
  16. package/resources/threats/files.yaml +38 -0
  17. package/resources/threats/mac-commands.yaml +118 -27
  18. package/resources/threats/mac-credentials.yaml +34 -8
  19. package/resources/threats/mac-files.yaml +20 -0
  20. package/resources/threats/mac-obfuscation.yaml +28 -6
  21. package/resources/threats/mac-persistence.yaml +30 -4
  22. package/resources/threats/mac-supply-chain.yaml +12 -3
  23. package/resources/threats/mitre.yaml +325 -62
  24. package/resources/threats/obfuscation.yaml +33 -8
  25. package/resources/threats/persistence.yaml +40 -5
  26. package/resources/threats/prompt-injection.yaml +58 -0
  27. package/resources/threats/self-defense.yaml +15 -2
  28. package/resources/threats/supply_chain.yaml +6 -1
  29. package/resources/threats/urls.yaml +10 -0
  30. package/resources/threats/win-commands.yaml +294 -60
  31. package/resources/threats/win-credentials.yaml +68 -9
  32. package/resources/threats/win-files.yaml +20 -0
  33. package/resources/threats/win-obfuscation.yaml +56 -8
  34. package/resources/threats/win-persistence.yaml +45 -7
  35. package/resources/threats/win-supply-chain.yaml +5 -1
package/dist/index.cjs CHANGED
@@ -4316,7 +4316,8 @@ var init_types2 = __esm({
4316
4316
  VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
4317
4317
  ThreatSchema = external_exports.object({
4318
4318
  id: external_exports.string(),
4319
- version: external_exports.number().int().optional(),
4319
+ version: external_exports.number().int().positive(),
4320
+ detectionName: external_exports.string(),
4320
4321
  category: external_exports.string(),
4321
4322
  severity: VerdictSeveritySchema,
4322
4323
  confidence: external_exports.number(),
@@ -4418,6 +4419,7 @@ var init_types2 = __esm({
4418
4419
  sensitivity: SensitivitySchema.default("balanced"),
4419
4420
  disabled_threats: external_exports.array(external_exports.string()).default([]),
4420
4421
  announce_clean_scans: external_exports.boolean().default(true),
4422
+ manage_status_line: external_exports.boolean().default(true),
4421
4423
  brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
4422
4424
  community_iq: external_exports.boolean().default(true)
4423
4425
  });
@@ -14601,6 +14603,7 @@ async function logVerdict(config2, input) {
14601
14603
  severity: input.verdict.severity,
14602
14604
  reasons: input.verdict.reasons,
14603
14605
  source: input.verdict.source,
14606
+ threat_id: input.verdict.matchedThreatId,
14604
14607
  user_override: userOverride,
14605
14608
  signals: input.signals,
14606
14609
  tool_use_id: input.toolUseId
@@ -14608,6 +14611,9 @@ async function logVerdict(config2, input) {
14608
14611
  if (input.content !== void 0) {
14609
14612
  entry.content = input.content;
14610
14613
  }
14614
+ if (input.contentSnippet !== void 0) {
14615
+ entry.content_snippet = input.contentSnippet;
14616
+ }
14611
14617
  try {
14612
14618
  await appendEntry(config2, entry);
14613
14619
  } catch {
@@ -15439,7 +15445,7 @@ init_file_utils();
15439
15445
  var import_meta = {};
15440
15446
  function resolveVersion() {
15441
15447
  if (true)
15442
- return "0.12.0";
15448
+ return "0.13.0";
15443
15449
  try {
15444
15450
  const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15445
15451
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -16025,14 +16031,29 @@ var BundledPiProvider = class _BundledPiProvider {
16025
16031
  if (truncated.length === 0)
16026
16032
  return null;
16027
16033
  const loaded = await this.ensureLoaded();
16028
- if (!loaded)
16034
+ if (!loaded) {
16035
+ this.logger.info("PI model inference skipped", {
16036
+ context,
16037
+ reason: "model_unavailable"
16038
+ });
16029
16039
  return null;
16040
+ }
16030
16041
  const chunks = this.chunkText(truncated);
16042
+ const modelId = (0, import_node_path9.basename)(this.modelPath);
16043
+ this.logger.info("PI model inference started", {
16044
+ context,
16045
+ modelId,
16046
+ contentLength: content.length,
16047
+ truncatedContentLength: truncated.length,
16048
+ chunks: chunks.length
16049
+ });
16031
16050
  let maxRisk = 0;
16032
16051
  let maxChunk = "";
16052
+ let chunksScanned = 0;
16033
16053
  for (const chunk of chunks) {
16034
16054
  if (chunk.length < 10)
16035
16055
  continue;
16056
+ chunksScanned++;
16036
16057
  const risk = await this.classifyChunk(chunk);
16037
16058
  if (risk > maxRisk) {
16038
16059
  maxRisk = risk;
@@ -16044,13 +16065,21 @@ var BundledPiProvider = class _BundledPiProvider {
16044
16065
  const snippet = maxChunk.length > 80 ? `${maxChunk.slice(0, 77)}...` : maxChunk;
16045
16066
  findings.push(snippet);
16046
16067
  }
16047
- return {
16068
+ const result = {
16048
16069
  risk: maxRisk,
16049
16070
  findings,
16050
16071
  contentName: context,
16051
- modelId: (0, import_node_path9.basename)(this.modelPath),
16072
+ modelId,
16052
16073
  contentSnippet: maxChunk || void 0
16053
16074
  };
16075
+ this.logger.info("PI model inference completed", {
16076
+ context,
16077
+ modelId,
16078
+ risk: result.risk,
16079
+ findingsCount: result.findings.length,
16080
+ chunksScanned
16081
+ });
16082
+ return result;
16054
16083
  } catch (err) {
16055
16084
  this.logger.warn("PI check failed (fail-open)", {
16056
16085
  error: err instanceof Error ? err.message : String(err),
@@ -16289,7 +16318,7 @@ var import_node_path10 = require("node:path");
16289
16318
  init_file_utils();
16290
16319
  init_types2();
16291
16320
  var CONFIG_DEFAULTS_FILENAME = "config.default.json";
16292
- var CONFIG_DEFAULTS_SCHEMA_VERSION = 1;
16321
+ var CONFIG_DEFAULTS_SCHEMA_VERSION = 2;
16293
16322
  function buildConfigDefaults() {
16294
16323
  return {
16295
16324
  schema_version: CONFIG_DEFAULTS_SCHEMA_VERSION,
@@ -16483,6 +16512,56 @@ function isScannableContent(content) {
16483
16512
  return SNIFF_HTML.test(head) || SNIFF_HTML_TAGS.test(head) || SNIFF_JSON.test(head) || SNIFF_XML.test(head) || SNIFF_YAML.test(head) || SNIFF_SHEBANG_SHELL.test(head) || SNIFF_SHEBANG_PYTHON.test(head) || SNIFF_PYTHON.test(head) || SNIFF_JS_TS.test(head) || SNIFF_C_CPP.test(head) || SNIFF_KOTLIN.test(head) || SNIFF_CSV.test(head);
16484
16513
  }
16485
16514
 
16515
+ // ../core/dist/detection-names.js
16516
+ var PI_DETECTION_NAME = "Other:SagePromptInjectionML-A [Susp]";
16517
+ var REPORTING_ENGINE_SHORTHANDS = {
16518
+ package: "sgpk",
16519
+ amsi: "sgam",
16520
+ heuristics: "sghe",
16521
+ ml: "sgml"
16522
+ };
16523
+ var REPORTING_DETECTION_SUFFIX = "sage";
16524
+ function formatReportingDetectionName(canonicalName, engine, optionalData = "") {
16525
+ const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
16526
+ return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
16527
+ }
16528
+ var PACKAGE_DETECTION_NAMES = {
16529
+ not_found: "Other:SagePackageNotFound-A [Susp]",
16530
+ suspicious_age: "Other:SagePackageNew-A [Susp]",
16531
+ malicious: "Other:SagePackageMalicious-A [Trj]",
16532
+ unknown: "Other:SagePackageUnknown-A [Susp]"
16533
+ };
16534
+ var AMSI_DETECTION_NAMES = {
16535
+ detected: "Other:SageAmsiDetected-A [Heur]",
16536
+ blocked_by_admin: "Other:SageAmsiBlockedByAdmin-A [Heur]",
16537
+ unknown: "Other:SageAmsiUnknown-A [Susp]"
16538
+ };
16539
+ var AMSI_REPORTING_RULE_NAMES = {
16540
+ detected: "AMSI_DETECTED",
16541
+ blocked_by_admin: "AMSI_BLOCKED_BY_ADMIN",
16542
+ unknown: "AMSI_UNKNOWN"
16543
+ };
16544
+ function isCanonicalDetectionName(name) {
16545
+ return /^\w+:\w+-[A-Z]+ \[[^\]\r\n]+\]$/.test(name);
16546
+ }
16547
+ function packageDetectionName(verdict) {
16548
+ return PACKAGE_DETECTION_NAMES[verdict];
16549
+ }
16550
+ function amsiDetectionClass(amsiResult) {
16551
+ if (amsiResult >= 32768)
16552
+ return "detected";
16553
+ if (amsiResult >= 16384)
16554
+ return "blocked_by_admin";
16555
+ return "unknown";
16556
+ }
16557
+ function amsiDetectionName(amsiResult) {
16558
+ return AMSI_DETECTION_NAMES[amsiDetectionClass(amsiResult)];
16559
+ }
16560
+ function amsiReportingData(amsiResult) {
16561
+ const ruleName = AMSI_REPORTING_RULE_NAMES[amsiDetectionClass(amsiResult)];
16562
+ return `${ruleName}:${Math.trunc(amsiResult).toString(16)}`;
16563
+ }
16564
+
16486
16565
  // ../core/dist/extended-info.js
16487
16566
  var import_promises4 = require("node:fs/promises");
16488
16567
  var import_node_os3 = require("node:os");
@@ -16735,7 +16814,8 @@ async function sendCommunityIqTelemetry(args) {
16735
16814
  ...blocking ? { user_action: "blocked" } : {},
16736
16815
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16737
16816
  ...args.signals && Object.keys(args.signals).length > 0 ? { signals: args.signals } : {},
16738
- content: args.content ?? {}
16817
+ content: args.content ?? {},
16818
+ ...args.contentSnippet ? { content_snippet: args.contentSnippet } : {}
16739
16819
  },
16740
16820
  event_id: args.eventId,
16741
16821
  comment: ""
@@ -17640,6 +17720,7 @@ var PackageChecker = class {
17640
17720
  if (pkg.name.startsWith("@")) {
17641
17721
  results.push({
17642
17722
  packageName: pkg.name,
17723
+ packageVersion: pkg.version,
17643
17724
  registry: pkg.registry,
17644
17725
  verdict: "clean",
17645
17726
  confidence: 1,
@@ -17660,6 +17741,7 @@ var PackageChecker = class {
17660
17741
  } catch {
17661
17742
  return {
17662
17743
  packageName: pkg.name,
17744
+ packageVersion: pkg.version,
17663
17745
  registry: pkg.registry,
17664
17746
  verdict: "unknown",
17665
17747
  confidence: 0.6,
@@ -17669,6 +17751,7 @@ var PackageChecker = class {
17669
17751
  if (metadata === null) {
17670
17752
  return {
17671
17753
  packageName: pkg.name,
17754
+ packageVersion: pkg.version,
17672
17755
  registry: pkg.registry,
17673
17756
  verdict: "not_found",
17674
17757
  confidence: 0.95,
@@ -17678,6 +17761,7 @@ var PackageChecker = class {
17678
17761
  if (!metadata.requestedVersionFound && pkg.version) {
17679
17762
  return {
17680
17763
  packageName: pkg.name,
17764
+ packageVersion: pkg.version,
17681
17765
  registry: pkg.registry,
17682
17766
  verdict: "not_found",
17683
17767
  confidence: 0.95,
@@ -17693,6 +17777,7 @@ var PackageChecker = class {
17693
17777
  const detections = fileResult.detectionNames.length > 0 ? fileResult.detectionNames.join(", ") : sev;
17694
17778
  return {
17695
17779
  packageName: pkg.name,
17780
+ packageVersion: pkg.version,
17696
17781
  registry: pkg.registry,
17697
17782
  verdict: "malicious",
17698
17783
  confidence: 1,
@@ -17711,6 +17796,7 @@ var PackageChecker = class {
17711
17796
  if (ageDays !== void 0 && ageDays < SUSPICIOUS_AGE_DAYS) {
17712
17797
  return {
17713
17798
  packageName: pkg.name,
17799
+ packageVersion: pkg.version,
17714
17800
  registry: pkg.registry,
17715
17801
  verdict: "suspicious_age",
17716
17802
  confidence: 0.6,
@@ -17720,6 +17806,7 @@ var PackageChecker = class {
17720
17806
  }
17721
17807
  return {
17722
17808
  packageName: pkg.name,
17809
+ packageVersion: pkg.version,
17723
17810
  registry: pkg.registry,
17724
17811
  verdict: "clean",
17725
17812
  confidence: 1,
@@ -18155,6 +18242,8 @@ init_file_utils();
18155
18242
  init_types2();
18156
18243
  var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
18157
18244
  "id",
18245
+ "version",
18246
+ "detection_name",
18158
18247
  "category",
18159
18248
  "severity",
18160
18249
  "confidence",
@@ -18162,6 +18251,37 @@ var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
18162
18251
  "match_on",
18163
18252
  "title"
18164
18253
  ]);
18254
+ var MACRO_FILENAME = "_macros.yaml";
18255
+ var MAX_MACRO_PASSES = 5;
18256
+ var MACRO_REF = /\{\{([A-Z0-9_]+)\}\}/g;
18257
+ function expandMacros(pattern, macros) {
18258
+ let expanded = pattern;
18259
+ for (let pass = 0; pass <= MAX_MACRO_PASSES; pass++) {
18260
+ if (!expanded.includes("{{"))
18261
+ return expanded;
18262
+ if (pass === MAX_MACRO_PASSES) {
18263
+ throw new Error(`macro nesting exceeds ${MAX_MACRO_PASSES} passes`);
18264
+ }
18265
+ expanded = expanded.replace(MACRO_REF, (_match, name) => {
18266
+ const value = macros[name];
18267
+ if (value === void 0)
18268
+ throw new Error(`undefined macro {{${name}}}`);
18269
+ return value;
18270
+ });
18271
+ }
18272
+ return expanded;
18273
+ }
18274
+ function readMacros(filename, data, logger) {
18275
+ const macros = {};
18276
+ for (const [name, value] of Object.entries(data)) {
18277
+ if (typeof value !== "string") {
18278
+ logger.warn(`Skipping macro ${name} in ${filename}: not a string`);
18279
+ continue;
18280
+ }
18281
+ macros[name] = value;
18282
+ }
18283
+ return macros;
18284
+ }
18165
18285
  function parseExpiresAt(value) {
18166
18286
  if (value == null)
18167
18287
  return null;
@@ -18187,11 +18307,12 @@ async function loadThreats(threatDir, logger = nullLogger) {
18187
18307
  logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
18188
18308
  return threats;
18189
18309
  }
18310
+ const ruleFiles = [];
18311
+ let macros = {};
18190
18312
  for (const filename of files) {
18191
- const filePath = (0, import_node_path17.join)(threatDir, filename);
18192
18313
  let content;
18193
18314
  try {
18194
- content = await getFileContent(filePath);
18315
+ content = await getFileContent((0, import_node_path17.join)(threatDir, filename));
18195
18316
  } catch (e) {
18196
18317
  logger.warn(`Failed to read ${filename}`, { error: String(e) });
18197
18318
  continue;
@@ -18203,10 +18324,21 @@ async function loadThreats(threatDir, logger = nullLogger) {
18203
18324
  logger.warn(`Failed to parse ${filename}`, { error: String(e) });
18204
18325
  continue;
18205
18326
  }
18327
+ if (filename === MACRO_FILENAME) {
18328
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
18329
+ logger.warn(`Expected mapping in ${filename}, got ${Array.isArray(data) ? "list" : typeof data}`);
18330
+ continue;
18331
+ }
18332
+ macros = readMacros(filename, data, logger);
18333
+ continue;
18334
+ }
18206
18335
  if (!Array.isArray(data)) {
18207
18336
  logger.warn(`Expected list in ${filename}, got ${typeof data}`);
18208
18337
  continue;
18209
18338
  }
18339
+ ruleFiles.push({ filename, data });
18340
+ }
18341
+ for (const { filename, data } of ruleFiles) {
18210
18342
  for (const entry of data) {
18211
18343
  if (typeof entry !== "object" || entry === null) {
18212
18344
  logger.warn(`Skipping non-object entry in ${filename}`);
@@ -18223,10 +18355,12 @@ async function loadThreats(threatDir, logger = nullLogger) {
18223
18355
  continue;
18224
18356
  if (isExpired(record))
18225
18357
  continue;
18358
+ let pattern;
18226
18359
  let compiledPattern;
18227
18360
  try {
18361
+ pattern = expandMacros(record.pattern, macros);
18228
18362
  const flags2 = record.case_insensitive === true ? "i" : "";
18229
- compiledPattern = new RegExp(record.pattern, flags2);
18363
+ compiledPattern = new RegExp(pattern, flags2);
18230
18364
  } catch (e) {
18231
18365
  logger.warn(`Skipping threat ${record.id}: invalid regex pattern`, {
18232
18366
  error: String(e)
@@ -18244,13 +18378,28 @@ async function loadThreats(threatDir, logger = nullLogger) {
18244
18378
  });
18245
18379
  continue;
18246
18380
  }
18381
+ const version = record.version;
18382
+ if (typeof version !== "number" || !Number.isInteger(version) || version <= 0) {
18383
+ logger.warn(`Skipping threat ${record.id}: invalid version`, {
18384
+ version
18385
+ });
18386
+ continue;
18387
+ }
18388
+ const detectionName = record.detection_name;
18389
+ if (typeof detectionName !== "string" || !isCanonicalDetectionName(detectionName)) {
18390
+ logger.warn(`Skipping threat ${record.id}: invalid detection_name`, {
18391
+ detectionName
18392
+ });
18393
+ continue;
18394
+ }
18247
18395
  threats.push({
18248
18396
  id: record.id,
18249
- version: typeof record.version === "number" ? record.version : void 0,
18397
+ version,
18398
+ detectionName,
18250
18399
  category: record.category,
18251
18400
  severity: record.severity,
18252
18401
  confidence,
18253
- pattern: record.pattern,
18402
+ pattern,
18254
18403
  compiledPattern,
18255
18404
  matchOn,
18256
18405
  title: record.title,
@@ -18268,6 +18417,29 @@ async function loadThreats(threatDir, logger = nullLogger) {
18268
18417
  init_types2();
18269
18418
  var AMSI_CONTENT_SNIPPET_MAX = 200;
18270
18419
  var AMSI_CONTENT_NAME_MAX = 256;
18420
+ var HEURISTIC_PI_CONTENT_SNIPPET_MAX = 200;
18421
+ function buildHeuristicSignal(match) {
18422
+ return {
18423
+ detection_name: formatReportingDetectionName(match.threat.detectionName, "heuristics", `${match.threat.id}:${match.threat.version}`),
18424
+ rule_id: match.threat.id,
18425
+ rule_version: match.threat.version
18426
+ };
18427
+ }
18428
+ function resolvePiContentSnippet(piResults, heuristicMatches) {
18429
+ let highestRiskResult;
18430
+ for (const result of piResults) {
18431
+ if (result.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && result.contentSnippet && (!highestRiskResult || result.risk > highestRiskResult.risk)) {
18432
+ highestRiskResult = result;
18433
+ }
18434
+ }
18435
+ if (highestRiskResult?.contentSnippet)
18436
+ return scrubHomePath(highestRiskResult.contentSnippet);
18437
+ const heuristicMatch = heuristicMatches.find((match) => match.threat.category === "prompt_injection");
18438
+ if (!heuristicMatch)
18439
+ return void 0;
18440
+ const snippet = safeTruncate(scrubHomePath(heuristicMatch.matchValue), HEURISTIC_PI_CONTENT_SNIPPET_MAX);
18441
+ return snippet || void 0;
18442
+ }
18271
18443
  function scrubAmsiContentName(contentName) {
18272
18444
  const colon = contentName.indexOf(":");
18273
18445
  if (colon < 0)
@@ -18277,16 +18449,10 @@ function scrubAmsiContentName(contentName) {
18277
18449
  return `${head}${scrubHomePath(tail)}`;
18278
18450
  }
18279
18451
  function buildAmsiSignal(r) {
18280
- const detectionName = r.amsiResult >= 32768 ? "AMSI|DETECTED" : r.amsiResult >= 16384 ? "AMSI|BLOCKED_BY_ADMIN" : (
18281
- // Defensive: callers should filter these out via `isDetected || isBlockedByAdmin`,
18282
- // but if a non-detected/non-blocked result still reaches here we emit a
18283
- // meaningful label rather than silently dropping the entry.
18284
- "AMSI|UNKNOWN"
18285
- );
18286
18452
  const contentName = safeTruncate(scrubAmsiContentName(r.contentName), AMSI_CONTENT_NAME_MAX);
18287
18453
  const snippet = r.content ? safeTruncate(scrubHomePath(r.content), AMSI_CONTENT_SNIPPET_MAX) : "";
18288
18454
  return {
18289
- detection_name: detectionName,
18455
+ detection_name: formatReportingDetectionName(amsiDetectionName(r.amsiResult), "amsi", amsiReportingData(r.amsiResult)),
18290
18456
  content_name: contentName,
18291
18457
  amsi_result: r.amsiResult,
18292
18458
  ...snippet ? { content_snippet: snippet } : {}
@@ -18358,7 +18524,7 @@ async function evaluateToolCall(request, context) {
18358
18524
  severity: "critical",
18359
18525
  source: "exception",
18360
18526
  artifacts: [denyMatch.artifact.value],
18361
- matchedThreatId: null,
18527
+ matchedThreatId: denyMatch.rule.id,
18362
18528
  reasons: [
18363
18529
  `Deny exception: ${denyMatch.rule.match} pattern '${denyMatch.rule.pattern}'${denyMatch.rule.reason ? ` \u2014 ${denyMatch.rule.reason}` : ""}`
18364
18530
  ]
@@ -18528,6 +18694,12 @@ async function evaluateToolCall(request, context) {
18528
18694
  if (result) {
18529
18695
  allPiResults.push(result);
18530
18696
  }
18697
+ } else {
18698
+ logger.info("PI model inference skipped", {
18699
+ context: `WebFetch:${url}`,
18700
+ reason: "content_not_scannable",
18701
+ contentType: fetched.contentType ?? "unknown"
18702
+ });
18531
18703
  }
18532
18704
  }
18533
18705
  }
@@ -18573,24 +18745,9 @@ async function evaluateToolCall(request, context) {
18573
18745
  }
18574
18746
  }
18575
18747
  await cacheUrlResults(urlCheckResults, cache2);
18576
- function formatPackageDetectionName(p) {
18577
- const base = `PKG|${p.verdict}|registry=${p.registry}|name=${p.packageName}`;
18578
- if (p.verdict === "suspicious_age") {
18579
- const ageDays = typeof p.ageDays === "number" ? Math.floor(p.ageDays) : void 0;
18580
- return ageDays !== void 0 ? `${base}|age_days=${ageDays}` : base;
18581
- }
18582
- if (p.verdict === "malicious") {
18583
- const det = (p.fileDetectionNames ?? []).filter((d) => typeof d === "string" && d.length > 0);
18584
- return det.length > 0 ? `${base}|det=${det.join(",")}` : base;
18585
- }
18586
- return base;
18587
- }
18588
18748
  const auditSignals = {};
18589
18749
  if (heuristicMatches.length > 0) {
18590
- auditSignals.heuristics = heuristicMatches.map((m) => ({
18591
- rule_id: m.threat.id,
18592
- rule_version: typeof m.threat.version === "number" ? m.threat.version : void 0
18593
- }));
18750
+ auditSignals.heuristics = heuristicMatches.map(buildHeuristicSignal);
18594
18751
  }
18595
18752
  if (urlCheckResults.length > 0) {
18596
18753
  const relevant = urlCheckResults.filter((r) => r.isMalicious);
@@ -18621,9 +18778,9 @@ async function evaluateToolCall(request, context) {
18621
18778
  const relevant = packageCheckResults.filter((p) => p.verdict !== "clean");
18622
18779
  if (relevant.length > 0) {
18623
18780
  auditSignals.package_checks = relevant.map((p) => ({
18624
- detection_name: formatPackageDetectionName(p),
18781
+ detection_name: formatReportingDetectionName(packageDetectionName(p.verdict), "package", p.packageVersion ? `${p.packageName}:${p.packageVersion}` : p.packageName),
18625
18782
  package_name: p.packageName,
18626
- package_version: void 0,
18783
+ package_version: p.packageVersion,
18627
18784
  package_registry: p.registry
18628
18785
  }));
18629
18786
  }
@@ -18638,6 +18795,7 @@ async function evaluateToolCall(request, context) {
18638
18795
  if (allPiResults.length > 0) {
18639
18796
  const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
18640
18797
  auditSignals.pi_checks = allPiResults.map((r) => ({
18798
+ detection_name: formatReportingDetectionName(PI_DETECTION_NAME, "ml", `${r.modelId}:${MODEL_SCHEMA_VERSION}`),
18641
18799
  risk: r.risk,
18642
18800
  model_id: r.modelId,
18643
18801
  content_name: r.contentName,
@@ -18651,6 +18809,7 @@ async function evaluateToolCall(request, context) {
18651
18809
  }
18652
18810
  }
18653
18811
  const resolvedSignals = Object.keys(auditSignals).length > 0 ? auditSignals : void 0;
18812
+ const contentSnippet = resolvePiContentSnippet(allPiResults, heuristicMatches);
18654
18813
  const builtContent = buildContentSnapshot(request.toolName, request.toolInput, request.artifacts, auditSignals);
18655
18814
  const resolvedContent = Object.keys(builtContent).length > 0 ? builtContent : void 0;
18656
18815
  try {
@@ -18664,6 +18823,7 @@ async function evaluateToolCall(request, context) {
18664
18823
  hookType: request.hookType,
18665
18824
  signals: resolvedSignals,
18666
18825
  content: resolvedContent,
18826
+ contentSnippet,
18667
18827
  eventId,
18668
18828
  toolUseId: request.toolUseId
18669
18829
  });
@@ -18679,6 +18839,7 @@ async function evaluateToolCall(request, context) {
18679
18839
  hookType: request.hookType,
18680
18840
  toolName: request.toolName,
18681
18841
  content: resolvedContent,
18842
+ contentSnippet,
18682
18843
  signals: resolvedSignals,
18683
18844
  communityIqEnabled: config2.community_iq,
18684
18845
  config: config2,
@@ -18804,6 +18965,7 @@ async function checkPackages(request, config2, cache2, logger) {
18804
18965
  }
18805
18966
  results.push({
18806
18967
  packageName: pkg.name,
18968
+ packageVersion: pkg.version,
18807
18969
  registry: pkg.registry,
18808
18970
  verdict: rawVerdict,
18809
18971
  confidence: rawConf,
@@ -18868,6 +19030,19 @@ function kv(key, value) {
18868
19030
  function separatorLine(headerLength) {
18869
19031
  return "\u2501".repeat(headerLength);
18870
19032
  }
19033
+ function ruleLabel(verdict) {
19034
+ if (!verdict.matchedThreatId)
19035
+ return null;
19036
+ return verdict.source === "exception" ? `${verdict.matchedThreatId} (your exception rule)` : verdict.matchedThreatId;
19037
+ }
19038
+ function remediationHint(verdict) {
19039
+ if (verdict.source === "exception") {
19040
+ const id = verdict.matchedThreatId ? ` (id ${verdict.matchedThreatId})` : "";
19041
+ return `This block comes from your own rule in ~/.sage/exceptions.json${id} \u2014 edit or remove it there.`;
19042
+ }
19043
+ const rule = verdict.matchedThreatId ? ` (rule ${verdict.matchedThreatId})` : "";
19044
+ return `If this is a false positive, use the sage_report_false_positive MCP tool to report it${rule}.`;
19045
+ }
18871
19046
  function formatUpdateNotice(result) {
18872
19047
  return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/gendigitalinc/sage)`;
18873
19048
  }
@@ -19014,12 +19189,14 @@ function summarizeArtifacts(artifacts) {
19014
19189
  function formatDenyMessage(verdict, branding2 = defaultBranding) {
19015
19190
  const maxReasons = 5;
19016
19191
  const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, maxReasons).join("; ") + (verdict.reasons.length > maxReasons ? `; ... and ${verdict.reasons.length - maxReasons} more` : "") : verdict.category;
19192
+ const rule = ruleLabel(verdict);
19017
19193
  return [
19018
19194
  `${branding2.name} blocked this action.`,
19019
19195
  `Severity: ${verdict.severity}`,
19020
19196
  `Category: ${verdict.category}`,
19197
+ ...rule ? [`Rule: ${rule}`] : [],
19021
19198
  `Reason: ${reasons}`,
19022
- `If this is a false positive, use the sage_report_false_positive MCP tool to report it.`
19199
+ remediationHint(verdict)
19023
19200
  ].join("\n");
19024
19201
  }
19025
19202
 
@@ -19688,6 +19865,10 @@ async function computeSkillIdsForRoot(rootDir, logger) {
19688
19865
  for (const folder of folders) {
19689
19866
  try {
19690
19867
  const entries = await entriesFromDirectory(folder, MAX_SKILL_BYTES);
19868
+ if (entries.length === 0) {
19869
+ logger?.warn("Skill enumerates to no entries; not analyzable, skipping", { folder });
19870
+ continue;
19871
+ }
19691
19872
  const { skillId } = computeSkillId(entries);
19692
19873
  out.push({ folder, skillId });
19693
19874
  } catch (e) {
@@ -20869,7 +21050,7 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
20869
21050
  branding: branding2,
20870
21051
  onResult,
20871
21052
  // No onNotices: OpenClaw delivers the one-time notice from
20872
- // `before_agent_start` (a different process/turn than this scan) via
21053
+ // `before_prompt_build` (a different process/turn than this scan) via
20873
21054
  // takePendingNotices, so the scan-time callback would never fire.
20874
21055
  modelDownloadWorkerPath: (0, import_node_path28.resolve)(__dirname, "model-download-worker.cjs"),
20875
21056
  skillUploadWorkerPath: (0, import_node_path28.resolve)(__dirname, "skill-upload-worker.cjs"),
@@ -20884,15 +21065,15 @@ function createStartupScanHandler(logger, branding2 = defaultBranding, onResult,
20884
21065
  function createSessionScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
20885
21066
  return createOpenClawScanHandler(logger, "session", branding2, onResult, announceCleanScans);
20886
21067
  }
20887
- function createBeforeAgentStartHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
21068
+ function createPromptContextHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
20888
21069
  return (notices) => {
20889
21070
  const findings = getSecurityFindings();
20890
21071
  if (!notices && !findings) {
20891
- logger.debug(`${branding2.name}: before_agent_start - nothing pending to surface`);
21072
+ logger.debug(`${branding2.name}: before_prompt_build - nothing pending to surface`);
20892
21073
  return void 0;
20893
21074
  }
20894
21075
  if (findings) clearFindings();
20895
- logger.debug(`${branding2.name}: surfacing context via before_agent_start`, {
21076
+ logger.debug(`${branding2.name}: surfacing context via before_prompt_build`, {
20896
21077
  hasNotices: Boolean(notices),
20897
21078
  hasFindings: Boolean(findings)
20898
21079
  });
@@ -21057,6 +21238,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
21057
21238
  }
21058
21239
  const maxReasons = 3;
21059
21240
  const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, maxReasons).join("; ") + (verdict.reasons.length > maxReasons ? `; ... and ${verdict.reasons.length - maxReasons} more` : "") : verdict.category;
21241
+ const rule = ruleLabel(verdict);
21060
21242
  const intersected = artifacts.filter((a) => verdict.artifacts.includes(a.value));
21061
21243
  const flaggedArtifacts = intersected.length > 0 ? intersected : artifacts;
21062
21244
  completeHook("evaluated", {
@@ -21073,6 +21255,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
21073
21255
  title: `${branding2.name}: ${verdict.category}`,
21074
21256
  description: [
21075
21257
  `Severity: ${verdict.severity}`,
21258
+ ...rule ? [`Rule: ${rule}`] : [],
21076
21259
  `Reason: ${reasons}`,
21077
21260
  `Artifacts: ${summarizeArtifacts(artifacts)}`
21078
21261
  ].join("\n"),
@@ -21143,7 +21326,7 @@ ${notice}` : notice;
21143
21326
  pendingScanFindings = msg;
21144
21327
  };
21145
21328
  const getPendingSecurityFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
21146
- const beforeAgentStartHandler = createBeforeAgentStartHandler(
21329
+ const promptContextHandler = createPromptContextHandler(
21147
21330
  getPendingSecurityFindings,
21148
21331
  () => {
21149
21332
  pendingConfigurationWarnings = null;
@@ -21179,10 +21362,10 @@ ${notice}` : notice;
21179
21362
  "session_start",
21180
21363
  createSessionScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
21181
21364
  );
21182
- api.on("before_agent_start", async () => {
21365
+ api.on("before_prompt_build", async () => {
21183
21366
  await migrationCheckPromise;
21184
21367
  const notices = await resolveNoticeText();
21185
- return beforeAgentStartHandler(notices);
21368
+ return promptContextHandler(notices);
21186
21369
  });
21187
21370
  }
21188
21371
  };