@gendigital/sage-openclaw 0.13.0 → 0.14.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
@@ -14500,6 +14500,84 @@ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {})
14500
14500
  return content;
14501
14501
  }
14502
14502
 
14503
+ // ../core/dist/detection-names.js
14504
+ var PI_DETECTION_NAME = "Other:SagePromptInjectionML-A [Susp]";
14505
+ var REPORTING_ENGINE_SHORTHANDS = {
14506
+ package: "sgpk",
14507
+ amsi: "sgam",
14508
+ heuristics: "sghe",
14509
+ ml: "sgml",
14510
+ skill: "skan"
14511
+ };
14512
+ var REPORTING_DETECTION_SUFFIX = "sage";
14513
+ function formatReportingDetectionName(canonicalName, engine, optionalData = "") {
14514
+ const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
14515
+ return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
14516
+ }
14517
+ var PACKAGE_DETECTION_NAMES = {
14518
+ not_found: "Other:SagePackageNotFound-A [Susp]",
14519
+ suspicious_age: "Other:SagePackageNew-A [Susp]",
14520
+ malicious: "Other:SagePackageMalicious-A [Trj]",
14521
+ unknown: "Other:SagePackageUnknown-A [Susp]"
14522
+ };
14523
+ var AMSI_DETECTION_NAMES = {
14524
+ detected: "Other:SageAmsiDetected-A [Heur]",
14525
+ blocked_by_admin: "Other:SageAmsiBlockedByAdmin-A [Heur]",
14526
+ unknown: "Other:SageAmsiUnknown-A [Susp]"
14527
+ };
14528
+ var AMSI_REPORTING_RULE_NAMES = {
14529
+ detected: "AMSI_DETECTED",
14530
+ blocked_by_admin: "AMSI_BLOCKED_BY_ADMIN",
14531
+ unknown: "AMSI_UNKNOWN"
14532
+ };
14533
+ var SKILL_DETECTION_NAMES = {
14534
+ HIGH: "Other:SageSuspiciousSkill-A [Susp]",
14535
+ CRITICAL: "Other:SageMaliciousSkill-A [Trj]"
14536
+ };
14537
+ function isCanonicalDetectionName(name) {
14538
+ return /^\w+:\w+-[A-Z]+ \[[^\]\r\n]+\]$/.test(name);
14539
+ }
14540
+ function packageDetectionName(verdict) {
14541
+ return PACKAGE_DETECTION_NAMES[verdict];
14542
+ }
14543
+ function amsiDetectionClass(amsiResult) {
14544
+ if (amsiResult >= 32768)
14545
+ return "detected";
14546
+ if (amsiResult >= 16384)
14547
+ return "blocked_by_admin";
14548
+ return "unknown";
14549
+ }
14550
+ function amsiDetectionName(amsiResult) {
14551
+ return AMSI_DETECTION_NAMES[amsiDetectionClass(amsiResult)];
14552
+ }
14553
+ function amsiReportingData(amsiResult) {
14554
+ const ruleName = AMSI_REPORTING_RULE_NAMES[amsiDetectionClass(amsiResult)];
14555
+ return `${ruleName}:${Math.trunc(amsiResult).toString(16)}`;
14556
+ }
14557
+ function skillDetectionName(verdict) {
14558
+ return SKILL_DETECTION_NAMES[verdict.toUpperCase()];
14559
+ }
14560
+ function isSkillId(value) {
14561
+ return /^[0-9a-f]{64}$/i.test(value);
14562
+ }
14563
+ function skillReportingDetectionName(verdict, skillId) {
14564
+ const canonicalName = skillDetectionName(verdict);
14565
+ return canonicalName && isSkillId(skillId) ? formatReportingDetectionName(canonicalName, "skill", skillId.toLowerCase()) : void 0;
14566
+ }
14567
+ function buildSkillCheckSignal(skillId, analyzerVerdict, jobId) {
14568
+ const normalizedSkillId = skillId.toLowerCase();
14569
+ const normalizedVerdict = analyzerVerdict.toUpperCase();
14570
+ const detectionName = skillReportingDetectionName(normalizedVerdict, normalizedSkillId);
14571
+ if (!detectionName)
14572
+ return void 0;
14573
+ return {
14574
+ detection_name: detectionName,
14575
+ skill_id: normalizedSkillId,
14576
+ ...jobId ? { job_id: jobId } : {},
14577
+ analyzer_verdict: normalizedVerdict
14578
+ };
14579
+ }
14580
+
14503
14581
  // ../core/dist/audit-log.js
14504
14582
  init_file_utils();
14505
14583
 
@@ -14651,6 +14729,37 @@ async function logSkillQueued(config2, skillId, pluginKey, skillFolder) {
14651
14729
  } catch {
14652
14730
  }
14653
14731
  }
14732
+ async function logSkillVerdict(config2, skillId, status, verdict, summary, context = {}) {
14733
+ if (!config2.enabled)
14734
+ return false;
14735
+ const entry = {
14736
+ type: "skill_verdict",
14737
+ entry_id: context.entryId ?? (0, import_node_crypto3.randomUUID)(),
14738
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14739
+ skill_id: skillId,
14740
+ status
14741
+ };
14742
+ if (context.jobId !== void 0)
14743
+ entry.job_id = context.jobId;
14744
+ if (context.agentRuntime !== void 0)
14745
+ entry.agent_runtime = context.agentRuntime;
14746
+ if (verdict !== void 0)
14747
+ entry.verdict = verdict;
14748
+ if (summary !== void 0)
14749
+ entry.summary = summary.slice(0, MAX_SUMMARY_LEN);
14750
+ const skillSignal = status === "analyzed" && verdict !== void 0 ? buildSkillCheckSignal(skillId, verdict, context.jobId) : void 0;
14751
+ if (skillSignal) {
14752
+ entry.signals = {
14753
+ skill_checks: [skillSignal]
14754
+ };
14755
+ }
14756
+ try {
14757
+ await appendEntry(config2, entry);
14758
+ return true;
14759
+ } catch {
14760
+ return false;
14761
+ }
14762
+ }
14654
14763
 
14655
14764
  // ../core/dist/cache.js
14656
14765
  init_config();
@@ -15445,7 +15554,7 @@ init_file_utils();
15445
15554
  var import_meta = {};
15446
15555
  function resolveVersion() {
15447
15556
  if (true)
15448
- return "0.13.0";
15557
+ return "0.14.0";
15449
15558
  try {
15450
15559
  const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15451
15560
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -16303,6 +16412,7 @@ var SkillCheckClient = class {
16303
16412
  parseResult(skillId, raw) {
16304
16413
  return {
16305
16414
  skillId,
16415
+ jobId: typeof raw.job_id === "string" ? raw.job_id : void 0,
16306
16416
  verdict: typeof raw.verdict === "string" ? raw.verdict : void 0,
16307
16417
  summary: typeof raw.summary === "string" ? raw.summary : void 0
16308
16418
  };
@@ -16512,56 +16622,6 @@ function isScannableContent(content) {
16512
16622
  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);
16513
16623
  }
16514
16624
 
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
-
16565
16625
  // ../core/dist/extended-info.js
16566
16626
  var import_promises4 = require("node:fs/promises");
16567
16627
  var import_node_os3 = require("node:os");
@@ -18137,7 +18197,7 @@ function extractFromPackageJson(content) {
18137
18197
  continue;
18138
18198
  if (version === "" || version === "*")
18139
18199
  continue;
18140
- if (version.startsWith("file:") || version.startsWith("link:") || version.startsWith("git+") || version.startsWith("git:") || version.startsWith("http:") || version.startsWith("https:") || version.startsWith("workspace:") || version.startsWith("github:") || version.startsWith("bitbucket:") || version.startsWith("gitlab:") || version.startsWith("portal:") || version.startsWith("patch:"))
18200
+ if (version.startsWith("catalog:") || version.startsWith("file:") || version.startsWith("link:") || version.startsWith("git+") || version.startsWith("git:") || version.startsWith("http:") || version.startsWith("https:") || version.startsWith("workspace:") || version.startsWith("github:") || version.startsWith("bitbucket:") || version.startsWith("gitlab:") || version.startsWith("portal:") || version.startsWith("patch:"))
18141
18201
  continue;
18142
18202
  if (version.startsWith("npm:")) {
18143
18203
  const aliasTarget = version.slice(4);
@@ -19652,7 +19712,7 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
19652
19712
  }
19653
19713
 
19654
19714
  // ../core/dist/plugin-scanner.js
19655
- var import_node_crypto9 = require("node:crypto");
19715
+ var import_node_crypto10 = require("node:crypto");
19656
19716
  var import_promises11 = require("node:fs/promises");
19657
19717
  var import_node_path23 = require("node:path");
19658
19718
  init_config();
@@ -20017,7 +20077,7 @@ function mergeSources(existing, incoming) {
20017
20077
  function defaultCachePath2() {
20018
20078
  return (0, import_node_path22.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
20019
20079
  }
20020
- async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger = nullLogger) {
20080
+ async function loadSkillVerdictCacheInternal(cachePath, ttlMs, logger, pruneOnLoad) {
20021
20081
  let raw;
20022
20082
  try {
20023
20083
  raw = await getFileContent(cachePath);
@@ -20040,6 +20100,8 @@ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DE
20040
20100
  continue;
20041
20101
  }
20042
20102
  entries[skillId] = {
20103
+ entryId: typeof entry.entry_id === "string" ? entry.entry_id : void 0,
20104
+ jobId: typeof entry.job_id === "string" ? entry.job_id : void 0,
20043
20105
  verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
20044
20106
  summary: typeof entry.summary === "string" ? entry.summary : void 0,
20045
20107
  skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
@@ -20048,7 +20110,7 @@ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DE
20048
20110
  };
20049
20111
  }
20050
20112
  const cache2 = { entries };
20051
- if (pruned > 0) {
20113
+ if (pruneOnLoad && pruned > 0) {
20052
20114
  logger.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
20053
20115
  await saveSkillVerdictCache(cache2, cachePath, logger);
20054
20116
  }
@@ -20058,6 +20120,9 @@ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DE
20058
20120
  return { entries: {} };
20059
20121
  }
20060
20122
  }
20123
+ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger = nullLogger) {
20124
+ return loadSkillVerdictCacheInternal(cachePath, ttlMs, logger, true);
20125
+ }
20061
20126
  async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger = nullLogger) {
20062
20127
  try {
20063
20128
  const data = {
@@ -20065,6 +20130,8 @@ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), lo
20065
20130
  entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
20066
20131
  skillId,
20067
20132
  {
20133
+ entry_id: v.entryId,
20134
+ job_id: v.jobId,
20068
20135
  verdict: v.verdict,
20069
20136
  summary: v.summary,
20070
20137
  skill_name: v.skillName,
@@ -20077,8 +20144,10 @@ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), lo
20077
20144
  ]))
20078
20145
  };
20079
20146
  await atomicWriteJson(cachePath, data);
20147
+ return true;
20080
20148
  } catch (e) {
20081
20149
  logger.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
20150
+ return false;
20082
20151
  }
20083
20152
  }
20084
20153
  function getVerdict(cache2, skillId, ttlMs = DEFAULT_VERDICT_TTL_MS) {
@@ -20099,6 +20168,34 @@ function putVerdict(cache2, skillId, verdict) {
20099
20168
  };
20100
20169
  }
20101
20170
 
20171
+ // ../core/dist/skill-verdict-record.js
20172
+ var import_node_crypto9 = require("node:crypto");
20173
+ function recordAnalyzedSkillVerdict(cache2, skillId, result, origin) {
20174
+ const entryId = (0, import_node_crypto9.randomUUID)();
20175
+ putVerdict(cache2, skillId, {
20176
+ entryId,
20177
+ jobId: result.jobId,
20178
+ verdict: result.verdict,
20179
+ summary: result.summary,
20180
+ skillName: origin.skillName,
20181
+ sources: [{ agentRuntime: origin.agentRuntime, containerKey: origin.containerKey }]
20182
+ });
20183
+ const { loggingConfig } = origin;
20184
+ return {
20185
+ entryId,
20186
+ writeAuditEntry: async () => {
20187
+ if (!loggingConfig)
20188
+ return;
20189
+ await logSkillVerdict(loggingConfig, skillId, "analyzed", result.verdict, result.summary, {
20190
+ entryId,
20191
+ jobId: result.jobId,
20192
+ agentRuntime: origin.agentRuntime
20193
+ });
20194
+ }
20195
+ };
20196
+ }
20197
+ var SKILL_ENTRY_ID_NAMESPACE = Buffer.from("75bba79a376142a7bacbf95d791126d3", "hex");
20198
+
20102
20199
  // ../core/dist/plugin-scanner.js
20103
20200
  init_types2();
20104
20201
  var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
@@ -20226,7 +20323,7 @@ async function scanPlugin(plugin, options = {}) {
20226
20323
  allUrls.push(...extractUrls(content));
20227
20324
  }
20228
20325
  if (checkFileHashes) {
20229
- const sha256 = (0, import_node_crypto9.createHash)("sha256").update(rawBytes).digest("hex");
20326
+ const sha256 = (0, import_node_crypto10.createHash)("sha256").update(rawBytes).digest("hex");
20230
20327
  const existing = hashToFiles.get(sha256);
20231
20328
  if (existing) {
20232
20329
  existing.push(filePath);
@@ -20323,6 +20420,8 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
20323
20420
  let markerModified = false;
20324
20421
  let lookupFailed = false;
20325
20422
  let graceDeferred = false;
20423
+ const resolvedPendingSkillIds = /* @__PURE__ */ new Set();
20424
+ const recordedVerdicts = /* @__PURE__ */ new Map();
20326
20425
  for (const { folder, skillId } of uncached) {
20327
20426
  if (!verdicts.has(skillId)) {
20328
20427
  lookupFailed = true;
@@ -20330,17 +20429,18 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
20330
20429
  }
20331
20430
  const verdict = verdicts.get(skillId) ?? null;
20332
20431
  if (verdict) {
20333
- putVerdict(verdictCache, skillId, {
20334
- verdict: verdict.verdict,
20335
- summary: verdict.summary,
20336
- skillName: (0, import_node_path23.basename)(folder),
20337
- sources: [{ agentRuntime: paths.agentRuntime, containerKey: plugin.key }]
20338
- });
20339
- cacheModified = true;
20432
+ if (!recordedVerdicts.has(skillId)) {
20433
+ recordedVerdicts.set(skillId, recordAnalyzedSkillVerdict(verdictCache, skillId, verdict, {
20434
+ skillName: (0, import_node_path23.basename)(folder),
20435
+ agentRuntime: paths.agentRuntime,
20436
+ containerKey: plugin.key,
20437
+ loggingConfig: paths.loggingConfig
20438
+ }));
20439
+ cacheModified = true;
20440
+ }
20340
20441
  pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict);
20341
20442
  if (isPending(marker, skillId)) {
20342
- removePending(marker, skillId);
20343
- markerModified = true;
20443
+ resolvedPendingSkillIds.add(skillId);
20344
20444
  }
20345
20445
  } else if (!isPending(marker, skillId)) {
20346
20446
  if (paths.uploadEnabled === false) {
@@ -20359,8 +20459,17 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
20359
20459
  }
20360
20460
  }
20361
20461
  }
20362
- if (cacheModified)
20363
- await saveSkillVerdictCache(verdictCache, paths.verdictCachePath, logger);
20462
+ const cacheSaved = cacheModified ? await saveSkillVerdictCache(verdictCache, paths.verdictCachePath, logger) : true;
20463
+ if (cacheSaved) {
20464
+ await Promise.all([...recordedVerdicts.values()].map((entry) => entry.writeAuditEntry().catch(() => {
20465
+ })));
20466
+ for (const skillId of resolvedPendingSkillIds) {
20467
+ removePending(marker, skillId);
20468
+ markerModified = true;
20469
+ }
20470
+ } else {
20471
+ lookupFailed = true;
20472
+ }
20364
20473
  if (markerModified)
20365
20474
  await savePendingMarker(marker, paths.pendingPath, logger);
20366
20475
  const anyStillPending = uncached.some(({ skillId }) => isPending(marker, skillId));
@@ -21050,7 +21159,7 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
21050
21159
  branding: branding2,
21051
21160
  onResult,
21052
21161
  // No onNotices: OpenClaw delivers the one-time notice from
21053
- // `before_agent_start` (a different process/turn than this scan) via
21162
+ // `before_prompt_build` (a different process/turn than this scan) via
21054
21163
  // takePendingNotices, so the scan-time callback would never fire.
21055
21164
  modelDownloadWorkerPath: (0, import_node_path28.resolve)(__dirname, "model-download-worker.cjs"),
21056
21165
  skillUploadWorkerPath: (0, import_node_path28.resolve)(__dirname, "skill-upload-worker.cjs"),
@@ -21065,15 +21174,15 @@ function createStartupScanHandler(logger, branding2 = defaultBranding, onResult,
21065
21174
  function createSessionScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
21066
21175
  return createOpenClawScanHandler(logger, "session", branding2, onResult, announceCleanScans);
21067
21176
  }
21068
- function createBeforeAgentStartHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
21177
+ function createPromptContextHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
21069
21178
  return (notices) => {
21070
21179
  const findings = getSecurityFindings();
21071
21180
  if (!notices && !findings) {
21072
- logger.debug(`${branding2.name}: before_agent_start - nothing pending to surface`);
21181
+ logger.debug(`${branding2.name}: before_prompt_build - nothing pending to surface`);
21073
21182
  return void 0;
21074
21183
  }
21075
21184
  if (findings) clearFindings();
21076
- logger.debug(`${branding2.name}: surfacing context via before_agent_start`, {
21185
+ logger.debug(`${branding2.name}: surfacing context via before_prompt_build`, {
21077
21186
  hasNotices: Boolean(notices),
21078
21187
  hasFindings: Boolean(findings)
21079
21188
  });
@@ -21326,7 +21435,7 @@ ${notice}` : notice;
21326
21435
  pendingScanFindings = msg;
21327
21436
  };
21328
21437
  const getPendingSecurityFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
21329
- const beforeAgentStartHandler = createBeforeAgentStartHandler(
21438
+ const promptContextHandler = createPromptContextHandler(
21330
21439
  getPendingSecurityFindings,
21331
21440
  () => {
21332
21441
  pendingConfigurationWarnings = null;
@@ -21362,10 +21471,10 @@ ${notice}` : notice;
21362
21471
  "session_start",
21363
21472
  createSessionScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
21364
21473
  );
21365
- api.on("before_agent_start", async () => {
21474
+ api.on("before_prompt_build", async () => {
21366
21475
  await migrationCheckPromise;
21367
21476
  const notices = await resolveNoticeText();
21368
- return beforeAgentStartHandler(notices);
21477
+ return promptContextHandler(notices);
21369
21478
  });
21370
21479
  }
21371
21480
  };