@gendigital/sage-openclaw 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +1648 -489
  3. package/dist/index.cjs.map +4 -4
  4. package/dist/mcp-server.cjs +230 -96
  5. package/dist/mcp-server.cjs.map +4 -4
  6. package/dist/model-download-worker.cjs +139 -94
  7. package/dist/model-download-worker.cjs.map +3 -3
  8. package/dist/skill-upload-worker.cjs +5638 -0
  9. package/dist/skill-upload-worker.cjs.map +7 -0
  10. package/openclaw.plugin.json +6 -1
  11. package/package.json +8 -3
  12. package/resources/threats/_macros.yaml +193 -0
  13. package/resources/threats/agent-layer.yaml +19 -1
  14. package/resources/threats/commands.yaml +165 -31
  15. package/resources/threats/credentials.yaml +24 -4
  16. package/resources/threats/dummy.yaml +52 -17
  17. package/resources/threats/files.yaml +38 -0
  18. package/resources/threats/mac-commands.yaml +118 -27
  19. package/resources/threats/mac-credentials.yaml +34 -8
  20. package/resources/threats/mac-files.yaml +20 -0
  21. package/resources/threats/mac-obfuscation.yaml +28 -6
  22. package/resources/threats/mac-persistence.yaml +30 -4
  23. package/resources/threats/mac-supply-chain.yaml +12 -3
  24. package/resources/threats/mitre.yaml +325 -62
  25. package/resources/threats/obfuscation.yaml +33 -8
  26. package/resources/threats/persistence.yaml +40 -5
  27. package/resources/threats/prompt-injection.yaml +58 -0
  28. package/resources/threats/self-defense.yaml +15 -2
  29. package/resources/threats/supply_chain.yaml +6 -1
  30. package/resources/threats/urls.yaml +10 -0
  31. package/resources/threats/win-commands.yaml +294 -60
  32. package/resources/threats/win-credentials.yaml +68 -9
  33. package/resources/threats/win-files.yaml +20 -0
  34. package/resources/threats/win-obfuscation.yaml +56 -8
  35. package/resources/threats/win-persistence.yaml +45 -7
  36. package/resources/threats/win-supply-chain.yaml +5 -1
package/dist/index.cjs CHANGED
@@ -63,7 +63,7 @@ async function renameWithRetry(from, to, attempts = 5) {
63
63
  const transient = code === "EPERM" || code === "EACCES" || code === "EBUSY";
64
64
  if (!transient || i === attempts - 1)
65
65
  throw err;
66
- await new Promise((resolve9) => setTimeout(resolve9, 10 * 2 ** i));
66
+ await new Promise((resolve10) => setTimeout(resolve10, 10 * 2 ** i));
67
67
  }
68
68
  }
69
69
  }
@@ -130,7 +130,47 @@ async function pruneOrphanedModelDownloads(stagingDir, maxAgeMs = 60 * 60 * 1e3)
130
130
  }
131
131
  }
132
132
  }
133
- var import_node_crypto, fs, fsPromises, import_node_os, import_node_path, name1, name2;
133
+ async function removeStaleFileLock(lockPath, staleAgeMs) {
134
+ try {
135
+ const s = await fsPromises.stat(lockPath);
136
+ if (Date.now() - s.mtimeMs < staleAgeMs)
137
+ return;
138
+ await fsPromises.rmdir(lockPath);
139
+ } catch {
140
+ }
141
+ }
142
+ async function acquireFileLock(filePath, timeoutMs = 250, staleAgeMs = 3e4) {
143
+ const lockPath = `${filePath}.lock`;
144
+ const deadline = Date.now() + timeoutMs;
145
+ while (true) {
146
+ try {
147
+ await fsPromises.mkdir(lockPath);
148
+ return async () => {
149
+ try {
150
+ await fsPromises.rmdir(lockPath);
151
+ } catch {
152
+ }
153
+ };
154
+ } catch {
155
+ await removeStaleFileLock(lockPath, staleAgeMs);
156
+ const remainingMs = deadline - Date.now();
157
+ if (remainingMs <= 0)
158
+ return void 0;
159
+ await (0, import_promises.setTimeout)(Math.min(50, remainingMs));
160
+ }
161
+ }
162
+ }
163
+ async function withFileLock(filePath, callback) {
164
+ const release = await acquireFileLock(filePath);
165
+ if (!release)
166
+ return;
167
+ try {
168
+ return await callback();
169
+ } finally {
170
+ await release();
171
+ }
172
+ }
173
+ var import_node_crypto, fs, fsPromises, import_node_os, import_node_path, import_promises, name1, name2;
134
174
  var init_file_utils = __esm({
135
175
  "../core/dist/file-utils.js"() {
136
176
  "use strict";
@@ -139,6 +179,7 @@ var init_file_utils = __esm({
139
179
  fsPromises = __toESM(require("node:fs/promises"), 1);
140
180
  import_node_os = require("node:os");
141
181
  import_node_path = require("node:path");
182
+ import_promises = require("node:timers/promises");
142
183
  name1 = "read";
143
184
  name2 = "File";
144
185
  }
@@ -4251,7 +4292,7 @@ var init_zod = __esm({
4251
4292
  });
4252
4293
 
4253
4294
  // ../core/dist/types.js
4254
- var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4295
+ var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, SkillCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, DEFAULT_PI_TELEMETRY_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4255
4296
  var init_types2 = __esm({
4256
4297
  "../core/dist/types.js"() {
4257
4298
  "use strict";
@@ -4275,7 +4316,8 @@ var init_types2 = __esm({
4275
4316
  VerdictSeveritySchema = external_exports.enum(["info", "warning", "critical"]);
4276
4317
  ThreatSchema = external_exports.object({
4277
4318
  id: external_exports.string(),
4278
- version: external_exports.number().int().optional(),
4319
+ version: external_exports.number().int().positive(),
4320
+ detectionName: external_exports.string(),
4279
4321
  category: external_exports.string(),
4280
4322
  severity: VerdictSeveritySchema,
4281
4323
  confidence: external_exports.number(),
@@ -4328,14 +4370,24 @@ var init_types2 = __esm({
4328
4370
  AmsiCheckConfigSchema = external_exports.object({
4329
4371
  enabled: external_exports.boolean().default(true)
4330
4372
  });
4373
+ SkillCheckConfigSchema = external_exports.object({
4374
+ enabled: external_exports.boolean().default(true),
4375
+ cache_ttl_days: external_exports.number().min(0).default(1),
4376
+ /**
4377
+ * Upload unknown skills (never seen by the analyzer) for deep content
4378
+ * analysis. When false, the scan still looks skills up by content hash and
4379
+ * still flags known-risky ones — but no skill content ever leaves the
4380
+ * machine and the upload worker never runs (lookup-only mode).
4381
+ */
4382
+ upload_enabled: external_exports.boolean().default(true)
4383
+ });
4331
4384
  DEFAULT_PI_HIGH_RISK_THRESHOLD = 0.99;
4332
4385
  DEFAULT_PI_MEDIUM_RISK_THRESHOLD = 0.5;
4386
+ DEFAULT_PI_TELEMETRY_THRESHOLD = 0.95;
4333
4387
  PiCheckConfigSchema = external_exports.object({
4334
4388
  enabled: external_exports.boolean().default(false),
4335
4389
  max_content_length: external_exports.number().default(16384),
4336
- model_path: external_exports.string().optional(),
4337
- high_risk_threshold: external_exports.number().default(DEFAULT_PI_HIGH_RISK_THRESHOLD),
4338
- medium_risk_threshold: external_exports.number().default(DEFAULT_PI_MEDIUM_RISK_THRESHOLD)
4390
+ model_path: external_exports.string().optional()
4339
4391
  });
4340
4392
  ExceptionDecisionSchema = external_exports.enum(["allow", "deny"]);
4341
4393
  ExceptionMatchSchema = external_exports.enum(["executable", "domain", "path", "plugin", "regex"]);
@@ -4357,6 +4409,7 @@ var init_types2 = __esm({
4357
4409
  file_check: FileCheckConfigSchema.default({}),
4358
4410
  package_check: PackageCheckConfigSchema.default({}),
4359
4411
  amsi_check: AmsiCheckConfigSchema.default({}),
4412
+ skill_check: SkillCheckConfigSchema.default({}),
4360
4413
  pi_check: PiCheckConfigSchema.default({}),
4361
4414
  heuristics_enabled: external_exports.boolean().default(true),
4362
4415
  cache: CacheConfigSchema.default({}),
@@ -4365,6 +4418,8 @@ var init_types2 = __esm({
4365
4418
  operational_logging: OperationalLoggingConfigSchema.default({}),
4366
4419
  sensitivity: SensitivitySchema.default("balanced"),
4367
4420
  disabled_threats: external_exports.array(external_exports.string()).default([]),
4421
+ announce_clean_scans: external_exports.boolean().default(true),
4422
+ manage_status_line: external_exports.boolean().default(true),
4368
4423
  brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
4369
4424
  community_iq: external_exports.boolean().default(true)
4370
4425
  });
@@ -4383,13 +4438,27 @@ var init_types2 = __esm({
4383
4438
  var config_exports = {};
4384
4439
  __export(config_exports, {
4385
4440
  HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
4441
+ MS_PER_DAY: () => MS_PER_DAY,
4386
4442
  SAGE_DIR: () => SAGE_DIR,
4387
4443
  defaultConfigPath: () => defaultConfigPath,
4388
4444
  getClaudeConfigDir: () => getClaudeConfigDir,
4445
+ isFreshTimestamp: () => isFreshTimestamp,
4389
4446
  loadConfig: () => loadConfig,
4390
4447
  loadConfigSync: () => loadConfigSync,
4391
- resolvePath: () => resolvePath
4448
+ readExplicitSkillUploadEnabled: () => readExplicitSkillUploadEnabled,
4449
+ resolvePath: () => resolvePath,
4450
+ skillCacheTtlMs: () => skillCacheTtlMs
4392
4451
  });
4452
+ function skillCacheTtlMs(config2) {
4453
+ return Math.max(1, config2.skill_check.cache_ttl_days) * MS_PER_DAY;
4454
+ }
4455
+ function isFreshTimestamp(ts, ttlMs, now = Date.now()) {
4456
+ if (Number.isNaN(ts))
4457
+ return false;
4458
+ if (ts - now > CLOCK_SKEW_TOLERANCE_MS)
4459
+ return false;
4460
+ return now - ts < ttlMs;
4461
+ }
4393
4462
  function resolvedSageDir() {
4394
4463
  return resolvePath(SAGE_DIR);
4395
4464
  }
@@ -4464,7 +4533,7 @@ function sanitizeBrandKey(data, logger) {
4464
4533
  if (typeof brandKey === "string" && brandKey.length >= 1 && brandKey.length <= 32 && BRAND_KEY_RE.test(brandKey)) {
4465
4534
  return data;
4466
4535
  }
4467
- logger.warn(`Invalid brand_key in config \u2014 ignoring`, { brand_key: brandKey });
4536
+ logger.warn(`Invalid brand_key in config - ignoring`, { brand_key: brandKey });
4468
4537
  const { brand_key: _, ...rest } = data;
4469
4538
  return rest;
4470
4539
  }
@@ -4516,6 +4585,21 @@ function parseConfig(raw, path, logger) {
4516
4585
  return defaultConfig(logger);
4517
4586
  }
4518
4587
  }
4588
+ async function readExplicitSkillUploadEnabled(configPath, logger = nullLogger) {
4589
+ const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4590
+ try {
4591
+ const data = JSON.parse(await getFileContent(path));
4592
+ const skillCheck = data.skill_check;
4593
+ if (skillCheck && typeof skillCheck === "object" && !Array.isArray(skillCheck) && "upload_enabled" in skillCheck) {
4594
+ const value = skillCheck.upload_enabled;
4595
+ if (typeof value === "boolean")
4596
+ return { present: true, value };
4597
+ logger.warn("Config skill_check.upload_enabled is not a boolean; ignoring", { value });
4598
+ }
4599
+ } catch {
4600
+ }
4601
+ return { present: false, value: false };
4602
+ }
4519
4603
  async function loadConfig(configPath, logger = nullLogger) {
4520
4604
  const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4521
4605
  try {
@@ -4532,7 +4616,7 @@ function loadConfigSync(configPath, logger = nullLogger) {
4532
4616
  return defaultConfig(logger);
4533
4617
  }
4534
4618
  }
4535
- var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, BRAND_KEY_RE;
4619
+ var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
4536
4620
  var init_config = __esm({
4537
4621
  "../core/dist/config.js"() {
4538
4622
  "use strict";
@@ -4541,6 +4625,8 @@ var init_config = __esm({
4541
4625
  init_types2();
4542
4626
  SAGE_DIR = "~/.sage";
4543
4627
  HOOK_TIMEOUT_SECONDS = 8;
4628
+ MS_PER_DAY = 24 * 60 * 60 * 1e3;
4629
+ CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1e3;
4544
4630
  BRAND_KEY_RE = /^[a-z0-9_-]+$/u;
4545
4631
  }
4546
4632
  });
@@ -6805,7 +6891,7 @@ async function ensurePiDeps(modelPath, logger = nullLogger) {
6805
6891
  }
6806
6892
  }
6807
6893
  function runNpm(cwd, args, logger) {
6808
- return new Promise((resolve9, reject) => {
6894
+ return new Promise((resolve10, reject) => {
6809
6895
  const isWindows = process.platform === "win32";
6810
6896
  const npmCmd = isWindows ? "npm.cmd" : "npm";
6811
6897
  const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
@@ -6813,7 +6899,7 @@ function runNpm(cwd, args, logger) {
6813
6899
  logger.warn(`npm install error: ${stderr || error.message}`);
6814
6900
  reject(error);
6815
6901
  } else {
6816
- resolve9();
6902
+ resolve10();
6817
6903
  }
6818
6904
  });
6819
6905
  child.unref();
@@ -14139,7 +14225,7 @@ function resolveBranding(brandKey, logger) {
14139
14225
  return defaultBranding;
14140
14226
  const entry = BRANDS[brandKey];
14141
14227
  if (!entry) {
14142
- logger?.warn(`Unknown brand_key "${brandKey}" in config \u2014 using default branding`);
14228
+ logger?.warn(`Unknown brand_key "${brandKey}" in config - using default branding`);
14143
14229
  return defaultBranding;
14144
14230
  }
14145
14231
  return { ...entry, brand_key: brandKey };
@@ -14239,88 +14325,226 @@ var ApprovalStore = class {
14239
14325
  // ../core/dist/audit-log.js
14240
14326
  var import_node_crypto3 = require("node:crypto");
14241
14327
  init_config();
14328
+
14329
+ // ../core/dist/content-snapshot.js
14330
+ var import_node_os2 = require("node:os");
14331
+ var CONTENT_FIELD_LIMITS = Object.freeze({
14332
+ command: 512,
14333
+ url: 512,
14334
+ file_path: 512,
14335
+ package_name: 256,
14336
+ package_version: 128,
14337
+ package_registry: 128
14338
+ });
14339
+ function safeTruncate(value, maxLen) {
14340
+ if (maxLen <= 0)
14341
+ return "";
14342
+ if (value.length <= maxLen)
14343
+ return value;
14344
+ const cutIndex = maxLen;
14345
+ const codeUnit = value.charCodeAt(cutIndex - 1);
14346
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
14347
+ return value.slice(0, cutIndex - 1);
14348
+ }
14349
+ return value.slice(0, cutIndex);
14350
+ }
14351
+ function scrubHomePath(value) {
14352
+ const home = (0, import_node_os2.homedir)();
14353
+ if (!home)
14354
+ return value;
14355
+ const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
14356
+ if (!normalizedHome)
14357
+ return value;
14358
+ const normalizedValue = value.replace(/\\/g, "/");
14359
+ if (normalizedValue === normalizedHome)
14360
+ return "~";
14361
+ if (normalizedValue.startsWith(`${normalizedHome}/`)) {
14362
+ return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
14363
+ }
14364
+ return value;
14365
+ }
14366
+ function asString(v) {
14367
+ return typeof v === "string" && v.length > 0 ? v : void 0;
14368
+ }
14369
+ function resolveFilePath(toolInput) {
14370
+ return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
14371
+ }
14372
+ function resolveWebFetchUrl(toolInput) {
14373
+ const single = asString(toolInput.url);
14374
+ if (single)
14375
+ return single;
14376
+ const urls = toolInput.urls;
14377
+ if (Array.isArray(urls)) {
14378
+ for (const u of urls) {
14379
+ if (typeof u === "string" && u.length > 0)
14380
+ return u;
14381
+ }
14382
+ }
14383
+ return void 0;
14384
+ }
14385
+ function extractFirstApplyPatchPath(patchText) {
14386
+ if (!patchText)
14387
+ return void 0;
14388
+ const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
14389
+ if (headerMatch?.[1]) {
14390
+ const trimmed = headerMatch[1].trim();
14391
+ if (trimmed)
14392
+ return trimmed;
14393
+ }
14394
+ const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
14395
+ if (renameMatch?.[1]) {
14396
+ const raw = renameMatch[1].trim();
14397
+ const arrow = raw.indexOf(" -> ");
14398
+ if (arrow !== -1) {
14399
+ const src = raw.slice(0, arrow).trim();
14400
+ if (src)
14401
+ return src;
14402
+ const dst = raw.slice(arrow + 4).trim();
14403
+ if (dst)
14404
+ return dst;
14405
+ } else if (raw) {
14406
+ return raw;
14407
+ }
14408
+ }
14409
+ return void 0;
14410
+ }
14411
+ function extractMcpContent(toolInput) {
14412
+ const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
14413
+ const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
14414
+ return {
14415
+ command: asString(candidate.command),
14416
+ url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
14417
+ file_path: resolveFilePath(candidate)
14418
+ };
14419
+ }
14420
+ function firstUrlArtifact(artifacts) {
14421
+ for (const a of artifacts) {
14422
+ if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
14423
+ return a.value;
14424
+ }
14425
+ }
14426
+ return void 0;
14427
+ }
14428
+ function applyFieldLimits(content) {
14429
+ for (const [key, value] of Object.entries(content)) {
14430
+ if (typeof value !== "string")
14431
+ continue;
14432
+ let sanitized = value;
14433
+ if (key === "file_path" || key === "command") {
14434
+ sanitized = scrubHomePath(sanitized);
14435
+ }
14436
+ const limit = CONTENT_FIELD_LIMITS[key];
14437
+ if (limit && sanitized.length > limit) {
14438
+ sanitized = safeTruncate(sanitized, limit);
14439
+ }
14440
+ content[key] = sanitized;
14441
+ }
14442
+ }
14443
+ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
14444
+ const content = {};
14445
+ switch (toolType) {
14446
+ case "Bash": {
14447
+ const command = asString(toolInput.command);
14448
+ if (command)
14449
+ content.command = command;
14450
+ break;
14451
+ }
14452
+ case "WebFetch": {
14453
+ const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
14454
+ if (url)
14455
+ content.url = url;
14456
+ break;
14457
+ }
14458
+ case "Write":
14459
+ case "Edit":
14460
+ case "Read":
14461
+ case "Delete": {
14462
+ const filePath = resolveFilePath(toolInput);
14463
+ if (filePath)
14464
+ content.file_path = filePath;
14465
+ break;
14466
+ }
14467
+ case "ApplyPatch": {
14468
+ const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
14469
+ const filePath = extractFirstApplyPatchPath(patchText);
14470
+ if (filePath)
14471
+ content.file_path = filePath;
14472
+ break;
14473
+ }
14474
+ case "MCP": {
14475
+ const mcp = extractMcpContent(toolInput);
14476
+ if (mcp.command)
14477
+ content.command = mcp.command;
14478
+ if (mcp.url)
14479
+ content.url = mcp.url;
14480
+ if (mcp.file_path)
14481
+ content.file_path = mcp.file_path;
14482
+ break;
14483
+ }
14484
+ }
14485
+ if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
14486
+ const firstUrl = signals.url_checks[0]?.url;
14487
+ if (firstUrl)
14488
+ content.url = firstUrl;
14489
+ }
14490
+ if (signals.package_checks && signals.package_checks.length > 0) {
14491
+ const p = signals.package_checks[0];
14492
+ if (p?.package_name)
14493
+ content.package_name = p.package_name;
14494
+ if (p?.package_version)
14495
+ content.package_version = p.package_version;
14496
+ if (p?.package_registry)
14497
+ content.package_registry = p.package_registry;
14498
+ }
14499
+ applyFieldLimits(content);
14500
+ return content;
14501
+ }
14502
+
14503
+ // ../core/dist/audit-log.js
14242
14504
  init_file_utils();
14243
14505
 
14244
14506
  // ../core/dist/jsonl-log-writer.js
14245
- var import_promises = require("node:fs/promises");
14507
+ var import_promises2 = require("node:fs/promises");
14246
14508
  var import_node_path4 = require("node:path");
14247
- var import_promises2 = require("node:timers/promises");
14248
14509
  init_config();
14510
+ init_file_utils();
14249
14511
  var writeQueues = /* @__PURE__ */ new Map();
14250
- var ROTATE_LOCK_TIMEOUT_MS = 250;
14251
- var ROTATE_LOCK_STALE_MS = 3e4;
14252
- var ROTATE_LOCK_POLL_MS = 50;
14253
14512
  async function shouldRotate(filePath, maxBytes, maxFiles) {
14254
14513
  if (maxBytes <= 0 || maxFiles <= 0)
14255
14514
  return false;
14256
14515
  try {
14257
- const s = await (0, import_promises.stat)(filePath);
14516
+ const s = await (0, import_promises2.stat)(filePath);
14258
14517
  return s.size >= maxBytes;
14259
14518
  } catch {
14260
14519
  return false;
14261
14520
  }
14262
14521
  }
14263
- async function removeStaleRotateLock(lockPath) {
14264
- try {
14265
- const s = await (0, import_promises.stat)(lockPath);
14266
- if (Date.now() - s.mtimeMs < ROTATE_LOCK_STALE_MS)
14267
- return;
14268
- await (0, import_promises.rmdir)(lockPath);
14269
- } catch {
14270
- }
14271
- }
14272
- async function acquireRotateLock(filePath) {
14273
- const lockPath = `${filePath}.lock`;
14274
- const deadline = Date.now() + ROTATE_LOCK_TIMEOUT_MS;
14275
- while (true) {
14276
- try {
14277
- await (0, import_promises.mkdir)(lockPath);
14278
- return async () => {
14279
- try {
14280
- await (0, import_promises.rmdir)(lockPath);
14281
- } catch {
14282
- }
14283
- };
14284
- } catch {
14285
- await removeStaleRotateLock(lockPath);
14286
- const remainingMs = deadline - Date.now();
14287
- if (remainingMs <= 0)
14288
- return void 0;
14289
- await (0, import_promises2.setTimeout)(Math.min(ROTATE_LOCK_POLL_MS, remainingMs));
14290
- }
14291
- }
14292
- }
14293
14522
  async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
14294
14523
  if (!await shouldRotate(filePath, maxBytes, maxFiles))
14295
14524
  return;
14296
- const releaseLock = await acquireRotateLock(filePath);
14297
- if (!releaseLock)
14298
- return;
14299
- try {
14525
+ await withFileLock(filePath, async () => {
14300
14526
  if (!await shouldRotate(filePath, maxBytes, maxFiles))
14301
14527
  return;
14302
14528
  try {
14303
- await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
14529
+ await (0, import_promises2.unlink)(`${filePath}.${maxFiles}`);
14304
14530
  } catch {
14305
14531
  }
14306
14532
  for (let i = maxFiles - 1; i >= 1; i--) {
14307
14533
  try {
14308
- await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14534
+ await (0, import_promises2.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14309
14535
  } catch {
14310
14536
  }
14311
14537
  }
14312
14538
  try {
14313
- await (0, import_promises.rename)(filePath, `${filePath}.1`);
14539
+ await (0, import_promises2.rename)(filePath, `${filePath}.1`);
14314
14540
  } catch {
14315
14541
  }
14316
- } finally {
14317
- await releaseLock();
14318
- }
14542
+ });
14319
14543
  }
14320
14544
  async function appendJsonlEntryNow(path, config2, entry) {
14321
- await (0, import_promises.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
14545
+ await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
14322
14546
  await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
14323
- await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
14547
+ await (0, import_promises2.appendFile)(path, `${JSON.stringify(entry)}
14324
14548
  `);
14325
14549
  }
14326
14550
  async function appendJsonlEntry(config2, entry) {
@@ -14338,6 +14562,7 @@ async function appendJsonlEntry(config2, entry) {
14338
14562
  }
14339
14563
 
14340
14564
  // ../core/dist/audit-log.js
14565
+ init_types2();
14341
14566
  var MAX_SUMMARY_LEN = 200;
14342
14567
  var AUDIT_LOG_SCHEMA_VERSION = 1;
14343
14568
  function toolInputSummary(toolName, toolInput) {
@@ -14378,6 +14603,7 @@ async function logVerdict(config2, input) {
14378
14603
  severity: input.verdict.severity,
14379
14604
  reasons: input.verdict.reasons,
14380
14605
  source: input.verdict.source,
14606
+ threat_id: input.verdict.matchedThreatId,
14381
14607
  user_override: userOverride,
14382
14608
  signals: input.signals,
14383
14609
  tool_use_id: input.toolUseId
@@ -14385,6 +14611,9 @@ async function logVerdict(config2, input) {
14385
14611
  if (input.content !== void 0) {
14386
14612
  entry.content = input.content;
14387
14613
  }
14614
+ if (input.contentSnippet !== void 0) {
14615
+ entry.content_snippet = input.contentSnippet;
14616
+ }
14388
14617
  try {
14389
14618
  await appendEntry(config2, entry);
14390
14619
  } catch {
@@ -14406,6 +14635,22 @@ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
14406
14635
  } catch {
14407
14636
  }
14408
14637
  }
14638
+ async function logSkillQueued(config2, skillId, pluginKey, skillFolder) {
14639
+ if (!config2.enabled)
14640
+ return;
14641
+ try {
14642
+ await appendEntry(config2, {
14643
+ type: "skill_queued",
14644
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14645
+ skill_id: skillId,
14646
+ plugin_key: pluginKey,
14647
+ // Home-scrubbed folder path: aids forensics when the same skill id shows
14648
+ // up under multiple plugins. Omitted when the caller has no path.
14649
+ ...skillFolder ? { skill_folder: scrubHomePath(skillFolder) } : {}
14650
+ });
14651
+ } catch {
14652
+ }
14653
+ }
14409
14654
 
14410
14655
  // ../core/dist/cache.js
14411
14656
  init_config();
@@ -14844,19 +15089,19 @@ var PersistentPowershellAmsiBackend = class {
14844
15089
  return this.available;
14845
15090
  }
14846
15091
  waitForLine(timeout) {
14847
- return new Promise((resolve9, reject) => {
15092
+ return new Promise((resolve10, reject) => {
14848
15093
  const timer = setTimeout(() => {
14849
15094
  this.pendingResponse = null;
14850
15095
  reject(new Error("timeout"));
14851
15096
  }, timeout);
14852
- this.pendingResponse = { resolve: resolve9, reject, timer };
15097
+ this.pendingResponse = { resolve: resolve10, reject, timer };
14853
15098
  });
14854
15099
  }
14855
15100
  async enqueueScan(operation) {
14856
15101
  const previous = this.scanQueue;
14857
15102
  let release;
14858
- this.scanQueue = new Promise((resolve9) => {
14859
- release = resolve9;
15103
+ this.scanQueue = new Promise((resolve10) => {
15104
+ release = resolve10;
14860
15105
  });
14861
15106
  await previous;
14862
15107
  try {
@@ -14904,10 +15149,10 @@ var PersistentPowershellAmsiBackend = class {
14904
15149
  const line = this.stdoutBuffer.slice(0, idx).trim();
14905
15150
  this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
14906
15151
  if (this.pendingResponse) {
14907
- const { resolve: resolve9, timer } = this.pendingResponse;
15152
+ const { resolve: resolve10, timer } = this.pendingResponse;
14908
15153
  this.pendingResponse = null;
14909
15154
  clearTimeout(timer);
14910
- resolve9(line);
15155
+ resolve10(line);
14911
15156
  }
14912
15157
  idx = this.stdoutBuffer.indexOf("\n");
14913
15158
  }
@@ -15006,7 +15251,7 @@ var WslPowershellAmsiBackend = class {
15006
15251
  if (!this.available)
15007
15252
  return null;
15008
15253
  const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
15009
- return new Promise((resolve9) => {
15254
+ return new Promise((resolve10) => {
15010
15255
  try {
15011
15256
  const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
15012
15257
  let stdout = "";
@@ -15016,7 +15261,7 @@ var WslPowershellAmsiBackend = class {
15016
15261
  ps.kill();
15017
15262
  } catch {
15018
15263
  }
15019
- resolve9(null);
15264
+ resolve10(null);
15020
15265
  }, PS_TIMEOUT);
15021
15266
  ps.stdout?.on("data", (chunk) => {
15022
15267
  stdout += chunk.toString();
@@ -15029,7 +15274,7 @@ var WslPowershellAmsiBackend = class {
15029
15274
  ps.on("error", (err) => {
15030
15275
  clearTimeout(timer);
15031
15276
  this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
15032
- resolve9(null);
15277
+ resolve10(null);
15033
15278
  });
15034
15279
  ps.on("exit", () => {
15035
15280
  clearTimeout(timer);
@@ -15042,11 +15287,11 @@ var WslPowershellAmsiBackend = class {
15042
15287
  contentName
15043
15288
  });
15044
15289
  }
15045
- resolve9(null);
15290
+ resolve10(null);
15046
15291
  return;
15047
15292
  }
15048
15293
  this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
15049
- resolve9(interpretAmsiResult(amsiResult, content, contentName));
15294
+ resolve10(interpretAmsiResult(amsiResult, content, contentName));
15050
15295
  });
15051
15296
  const req = JSON.stringify({ content: truncated, contentName });
15052
15297
  ps.stdin?.write(`${req}
@@ -15054,7 +15299,7 @@ var WslPowershellAmsiBackend = class {
15054
15299
  ps.stdin?.end();
15055
15300
  } catch (e) {
15056
15301
  this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
15057
- resolve9(null);
15302
+ resolve10(null);
15058
15303
  }
15059
15304
  });
15060
15305
  }
@@ -15200,7 +15445,7 @@ init_file_utils();
15200
15445
  var import_meta = {};
15201
15446
  function resolveVersion() {
15202
15447
  if (true)
15203
- return "0.11.0";
15448
+ return "0.13.0";
15204
15449
  try {
15205
15450
  const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15206
15451
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -15349,9 +15594,10 @@ var FileCheckClient = class {
15349
15594
  var import_node_fs = require("node:fs");
15350
15595
  var import_node_path6 = require("node:path");
15351
15596
  init_config();
15352
- var MODEL_SCHEMA_VERSION = "v1";
15597
+ var MODEL_SCHEMA_VERSION = "v2";
15353
15598
  var REQUIRED_MODELS_BY_SCHEMA = {
15354
- v1: ["pi-model"]
15599
+ v1: ["pi-model"],
15600
+ v2: ["pi-model"]
15355
15601
  };
15356
15602
  var REQUIRED_MODEL_FILES = {
15357
15603
  "pi-model": [
@@ -15385,8 +15631,11 @@ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
15385
15631
  init_types2();
15386
15632
  var STALE_LOCK_MS = 60 * 60 * 1e3;
15387
15633
 
15634
+ // ../core/dist/clients/model-manifest.js
15635
+ init_config();
15636
+
15388
15637
  // ../core/dist/sage-proxy.js
15389
- function mapSageProxyOs(platform) {
15638
+ function mapSageHostOs(platform) {
15390
15639
  switch (platform) {
15391
15640
  case "win32":
15392
15641
  return "WINDOWS";
@@ -15398,21 +15647,37 @@ function mapSageProxyOs(platform) {
15398
15647
  return platform;
15399
15648
  }
15400
15649
  }
15401
- function mapSageProxyArchitecture(arch) {
15650
+ function mapSageHostArchitecture(arch) {
15402
15651
  return arch.toUpperCase();
15403
15652
  }
15404
- function buildSageProxyEnvelope(args) {
15653
+ function buildSageUserConfig(config2) {
15405
15654
  return {
15406
- identity: { uuid: args.iid },
15655
+ sensitivity: config2.sensitivity,
15656
+ url_check_enabled: config2.url_check.enabled,
15657
+ file_check_enabled: config2.file_check.enabled,
15658
+ package_check_enabled: config2.package_check.enabled,
15659
+ heuristics_enabled: config2.heuristics_enabled,
15660
+ pi_check_enabled: config2.pi_check.enabled,
15661
+ community_iq_enabled: config2.community_iq,
15662
+ // Optional-chained + defaulted: real callers pass a loadConfig result (skill_check
15663
+ // always present via the schema default), but this feeds fail-open telemetry, so a
15664
+ // partial config must never crash the envelope build.
15665
+ skill_check_upload_enabled: config2.skill_check?.upload_enabled ?? true
15666
+ };
15667
+ }
15668
+ function buildSageProxyEnvelope(args) {
15669
+ return {
15670
+ identity: { uuid: args.iid },
15407
15671
  product: { version_app: args.versionApp },
15408
15672
  platform: {
15409
- os: args.platformOs ?? mapSageProxyOs(process.platform),
15410
- architecture: args.platformArchitecture ?? mapSageProxyArchitecture(process.arch)
15673
+ os: args.platformOs ?? mapSageHostOs(process.platform),
15674
+ architecture: args.platformArchitecture ?? mapSageHostArchitecture(process.arch)
15411
15675
  },
15412
15676
  agent: {
15413
15677
  agent_runtime: args.agentRuntime,
15414
15678
  agent_runtime_version: args.agentRuntimeVersion
15415
- }
15679
+ },
15680
+ ...args.config ? { config: buildSageUserConfig(args.config) } : {}
15416
15681
  };
15417
15682
  }
15418
15683
 
@@ -15766,14 +16031,29 @@ var BundledPiProvider = class _BundledPiProvider {
15766
16031
  if (truncated.length === 0)
15767
16032
  return null;
15768
16033
  const loaded = await this.ensureLoaded();
15769
- if (!loaded)
16034
+ if (!loaded) {
16035
+ this.logger.info("PI model inference skipped", {
16036
+ context,
16037
+ reason: "model_unavailable"
16038
+ });
15770
16039
  return null;
16040
+ }
15771
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
+ });
15772
16050
  let maxRisk = 0;
15773
16051
  let maxChunk = "";
16052
+ let chunksScanned = 0;
15774
16053
  for (const chunk of chunks) {
15775
16054
  if (chunk.length < 10)
15776
16055
  continue;
16056
+ chunksScanned++;
15777
16057
  const risk = await this.classifyChunk(chunk);
15778
16058
  if (risk > maxRisk) {
15779
16059
  maxRisk = risk;
@@ -15785,13 +16065,21 @@ var BundledPiProvider = class _BundledPiProvider {
15785
16065
  const snippet = maxChunk.length > 80 ? `${maxChunk.slice(0, 77)}...` : maxChunk;
15786
16066
  findings.push(snippet);
15787
16067
  }
15788
- return {
16068
+ const result = {
15789
16069
  risk: maxRisk,
15790
16070
  findings,
15791
16071
  contentName: context,
15792
- modelId: (0, import_node_path9.basename)(this.modelPath),
16072
+ modelId,
15793
16073
  contentSnippet: maxChunk || void 0
15794
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;
15795
16083
  } catch (err) {
15796
16084
  this.logger.warn("PI check failed (fail-open)", {
15797
16085
  error: err instanceof Error ? err.message : String(err),
@@ -15920,6 +16208,9 @@ ${content.slice(-tailLen)}`;
15920
16208
  // ../core/dist/index.js
15921
16209
  init_pi_deps_installer();
15922
16210
 
16211
+ // ../core/dist/clients/skill-analyze.js
16212
+ init_types2();
16213
+
15923
16214
  // ../core/dist/clients/skill-check.js
15924
16215
  init_types2();
15925
16216
  var DEFAULT_TIMEOUT4 = 5;
@@ -15942,9 +16233,13 @@ var SkillCheckClient = class {
15942
16233
  /**
15943
16234
  * Check a list of skill IDs against the proxy.
15944
16235
  *
15945
- * Returns a Map keyed by skill id. A `null` value means the proxy had
15946
- * no opinion on that skill (analogous to a clean verdict). Skill ids
15947
- * not found in the response are simply absent from the map.
16236
+ * Returns a Map keyed by skill id:
16237
+ * - `null` — proxy had no opinion (explicit null in response, or skill absent
16238
+ * from an otherwise-successful response). Callers should treat this as
16239
+ * "unknown, queue for upload".
16240
+ * - absent — the entire batch failed (HTTP error or network error). Callers
16241
+ * should fail open and skip the skill rather than enqueue it.
16242
+ * - object — a verdict from the analyzer.
15948
16243
  *
15949
16244
  * Fails-open: on any error returns an empty Map. Per-batch errors do
15950
16245
  * not poison results from successful batches.
@@ -15988,8 +16283,10 @@ var SkillCheckClient = class {
15988
16283
  const data = await response.json();
15989
16284
  const results = data.results ?? {};
15990
16285
  for (const id of skillIds) {
15991
- if (!(id in results))
16286
+ if (!(id in results)) {
16287
+ out.set(id, null);
15992
16288
  continue;
16289
+ }
15993
16290
  const raw = results[id];
15994
16291
  if (raw === null || raw === void 0) {
15995
16292
  out.set(id, null);
@@ -16004,17 +16301,10 @@ var SkillCheckClient = class {
16004
16301
  return out;
16005
16302
  }
16006
16303
  parseResult(skillId, raw) {
16007
- const recommendationsRaw = raw.recommendations ?? [];
16008
- const recommendations = recommendationsRaw.filter((r) => typeof r === "string");
16009
- const categoriesRaw = raw.threat_categories ?? [];
16010
- const threatCategories = categoriesRaw.filter((c) => typeof c === "string");
16011
16304
  return {
16012
16305
  skillId,
16013
16306
  verdict: typeof raw.verdict === "string" ? raw.verdict : void 0,
16014
- overallRiskLevel: typeof raw.overall_risk_level === "string" ? raw.overall_risk_level : void 0,
16015
- summary: typeof raw.summary === "string" ? raw.summary : void 0,
16016
- recommendations,
16017
- threatCategories
16307
+ summary: typeof raw.summary === "string" ? raw.summary : void 0
16018
16308
  };
16019
16309
  }
16020
16310
  };
@@ -16022,6 +16312,70 @@ var SkillCheckClient = class {
16022
16312
  // ../core/dist/index.js
16023
16313
  init_config();
16024
16314
 
16315
+ // ../core/dist/config-defaults.js
16316
+ var import_promises3 = require("node:fs/promises");
16317
+ var import_node_path10 = require("node:path");
16318
+ init_file_utils();
16319
+ init_types2();
16320
+ var CONFIG_DEFAULTS_FILENAME = "config.default.json";
16321
+ var CONFIG_DEFAULTS_SCHEMA_VERSION = 2;
16322
+ function buildConfigDefaults() {
16323
+ return {
16324
+ schema_version: CONFIG_DEFAULTS_SCHEMA_VERSION,
16325
+ ...ConfigSchema.parse({})
16326
+ };
16327
+ }
16328
+ function serializeConfigDefaults() {
16329
+ return `${JSON.stringify(buildConfigDefaults(), null, 2)}
16330
+ `;
16331
+ }
16332
+ function readSchemaVersion(raw) {
16333
+ try {
16334
+ const data = JSON.parse(raw);
16335
+ if (typeof data !== "object" || data === null || Array.isArray(data))
16336
+ return null;
16337
+ const version = data.schema_version;
16338
+ return typeof version === "number" && Number.isInteger(version) ? version : null;
16339
+ } catch {
16340
+ return null;
16341
+ }
16342
+ }
16343
+ async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
16344
+ const targetPath = (0, import_node_path10.join)(sageDirPath, CONFIG_DEFAULTS_FILENAME);
16345
+ const defaults = serializeConfigDefaults();
16346
+ try {
16347
+ await (0, import_promises3.mkdir)(sageDirPath, { recursive: true, mode: 448 });
16348
+ await (0, import_promises3.chmod)(sageDirPath, 448);
16349
+ let existing = null;
16350
+ try {
16351
+ existing = await (0, import_promises3.readFile)(targetPath, "utf-8");
16352
+ } catch {
16353
+ }
16354
+ if (existing) {
16355
+ const existingVersion = readSchemaVersion(existing);
16356
+ if (existingVersion !== null && existingVersion > CONFIG_DEFAULTS_SCHEMA_VERSION) {
16357
+ logger.warn("Deployed config defaults are newer, update your installation", {
16358
+ path: targetPath,
16359
+ existingVersion,
16360
+ supportedVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
16361
+ });
16362
+ return;
16363
+ }
16364
+ if (existing === defaults)
16365
+ return;
16366
+ }
16367
+ await withFileLock(targetPath, async () => {
16368
+ await (0, import_promises3.writeFile)(targetPath, defaults, { encoding: "utf-8", mode: 384 });
16369
+ logger.debug("Deployed config defaults", {
16370
+ path: targetPath,
16371
+ schemaVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
16372
+ });
16373
+ });
16374
+ } catch (err) {
16375
+ logger.debug("Failed to deploy config defaults", { path: targetPath, error: String(err) });
16376
+ }
16377
+ }
16378
+
16025
16379
  // ../core/dist/config-diagnostics.js
16026
16380
  init_config();
16027
16381
  init_file_utils();
@@ -16158,184 +16512,60 @@ function isScannableContent(content) {
16158
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);
16159
16513
  }
16160
16514
 
16161
- // ../core/dist/content-snapshot.js
16162
- var import_node_os2 = require("node:os");
16163
- var CONTENT_FIELD_LIMITS = Object.freeze({
16164
- command: 512,
16165
- url: 512,
16166
- file_path: 512,
16167
- package_name: 256,
16168
- package_version: 128,
16169
- package_registry: 128
16170
- });
16171
- function safeTruncate(value, maxLen) {
16172
- if (maxLen <= 0)
16173
- return "";
16174
- if (value.length <= maxLen)
16175
- return value;
16176
- const cutIndex = maxLen;
16177
- const codeUnit = value.charCodeAt(cutIndex - 1);
16178
- if (codeUnit >= 55296 && codeUnit <= 56319) {
16179
- return value.slice(0, cutIndex - 1);
16180
- }
16181
- return value.slice(0, cutIndex);
16182
- }
16183
- function scrubHomePath(value) {
16184
- const home = (0, import_node_os2.homedir)();
16185
- if (!home)
16186
- return value;
16187
- const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
16188
- if (!normalizedHome)
16189
- return value;
16190
- const normalizedValue = value.replace(/\\/g, "/");
16191
- if (normalizedValue === normalizedHome)
16192
- return "~";
16193
- if (normalizedValue.startsWith(`${normalizedHome}/`)) {
16194
- return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
16195
- }
16196
- return value;
16197
- }
16198
- function asString(v) {
16199
- return typeof v === "string" && v.length > 0 ? v : void 0;
16200
- }
16201
- function resolveFilePath(toolInput) {
16202
- return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
16203
- }
16204
- function resolveWebFetchUrl(toolInput) {
16205
- const single = asString(toolInput.url);
16206
- if (single)
16207
- return single;
16208
- const urls = toolInput.urls;
16209
- if (Array.isArray(urls)) {
16210
- for (const u of urls) {
16211
- if (typeof u === "string" && u.length > 0)
16212
- return u;
16213
- }
16214
- }
16215
- return void 0;
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}`;
16216
16527
  }
16217
- function extractFirstApplyPatchPath(patchText) {
16218
- if (!patchText)
16219
- return void 0;
16220
- const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
16221
- if (headerMatch?.[1]) {
16222
- const trimmed = headerMatch[1].trim();
16223
- if (trimmed)
16224
- return trimmed;
16225
- }
16226
- const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
16227
- if (renameMatch?.[1]) {
16228
- const raw = renameMatch[1].trim();
16229
- const arrow = raw.indexOf(" -> ");
16230
- if (arrow !== -1) {
16231
- const src = raw.slice(0, arrow).trim();
16232
- if (src)
16233
- return src;
16234
- const dst = raw.slice(arrow + 4).trim();
16235
- if (dst)
16236
- return dst;
16237
- } else if (raw) {
16238
- return raw;
16239
- }
16240
- }
16241
- return void 0;
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);
16242
16546
  }
16243
- function extractMcpContent(toolInput) {
16244
- const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
16245
- const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
16246
- return {
16247
- command: asString(candidate.command),
16248
- url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
16249
- file_path: resolveFilePath(candidate)
16250
- };
16547
+ function packageDetectionName(verdict) {
16548
+ return PACKAGE_DETECTION_NAMES[verdict];
16251
16549
  }
16252
- function firstUrlArtifact(artifacts) {
16253
- for (const a of artifacts) {
16254
- if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
16255
- return a.value;
16256
- }
16257
- }
16258
- return void 0;
16550
+ function amsiDetectionClass(amsiResult) {
16551
+ if (amsiResult >= 32768)
16552
+ return "detected";
16553
+ if (amsiResult >= 16384)
16554
+ return "blocked_by_admin";
16555
+ return "unknown";
16259
16556
  }
16260
- function applyFieldLimits(content) {
16261
- for (const [key, value] of Object.entries(content)) {
16262
- if (typeof value !== "string")
16263
- continue;
16264
- let sanitized = value;
16265
- if (key === "file_path" || key === "command") {
16266
- sanitized = scrubHomePath(sanitized);
16267
- }
16268
- const limit = CONTENT_FIELD_LIMITS[key];
16269
- if (limit && sanitized.length > limit) {
16270
- sanitized = safeTruncate(sanitized, limit);
16271
- }
16272
- content[key] = sanitized;
16273
- }
16557
+ function amsiDetectionName(amsiResult) {
16558
+ return AMSI_DETECTION_NAMES[amsiDetectionClass(amsiResult)];
16274
16559
  }
16275
- function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
16276
- const content = {};
16277
- switch (toolType) {
16278
- case "Bash": {
16279
- const command = asString(toolInput.command);
16280
- if (command)
16281
- content.command = command;
16282
- break;
16283
- }
16284
- case "WebFetch": {
16285
- const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
16286
- if (url)
16287
- content.url = url;
16288
- break;
16289
- }
16290
- case "Write":
16291
- case "Edit":
16292
- case "Read":
16293
- case "Delete": {
16294
- const filePath = resolveFilePath(toolInput);
16295
- if (filePath)
16296
- content.file_path = filePath;
16297
- break;
16298
- }
16299
- case "ApplyPatch": {
16300
- const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
16301
- const filePath = extractFirstApplyPatchPath(patchText);
16302
- if (filePath)
16303
- content.file_path = filePath;
16304
- break;
16305
- }
16306
- case "MCP": {
16307
- const mcp = extractMcpContent(toolInput);
16308
- if (mcp.command)
16309
- content.command = mcp.command;
16310
- if (mcp.url)
16311
- content.url = mcp.url;
16312
- if (mcp.file_path)
16313
- content.file_path = mcp.file_path;
16314
- break;
16315
- }
16316
- }
16317
- if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
16318
- const firstUrl = signals.url_checks[0]?.url;
16319
- if (firstUrl)
16320
- content.url = firstUrl;
16321
- }
16322
- if (signals.package_checks && signals.package_checks.length > 0) {
16323
- const p = signals.package_checks[0];
16324
- if (p?.package_name)
16325
- content.package_name = p.package_name;
16326
- if (p?.package_version)
16327
- content.package_version = p.package_version;
16328
- if (p?.package_registry)
16329
- content.package_registry = p.package_registry;
16330
- }
16331
- applyFieldLimits(content);
16332
- return content;
16560
+ function amsiReportingData(amsiResult) {
16561
+ const ruleName = AMSI_REPORTING_RULE_NAMES[amsiDetectionClass(amsiResult)];
16562
+ return `${ruleName}:${Math.trunc(amsiResult).toString(16)}`;
16333
16563
  }
16334
16564
 
16335
16565
  // ../core/dist/extended-info.js
16336
- var import_promises3 = require("node:fs/promises");
16566
+ var import_promises4 = require("node:fs/promises");
16337
16567
  var import_node_os3 = require("node:os");
16338
- var import_node_path10 = require("node:path");
16568
+ var import_node_path11 = require("node:path");
16339
16569
  init_file_utils();
16340
16570
  init_types2();
16341
16571
  var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
@@ -16401,8 +16631,8 @@ function sanitizeDocument(parsed, logger) {
16401
16631
  return out;
16402
16632
  }
16403
16633
  async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
16404
- const sageDir = sageDirPath ?? (0, import_node_path10.join)((0, import_node_os3.homedir)(), ".sage");
16405
- const filePath = (0, import_node_path10.join)(sageDir, EXTENDED_INFO_FILENAME);
16634
+ const sageDir = sageDirPath ?? (0, import_node_path11.join)((0, import_node_os3.homedir)(), ".sage");
16635
+ const filePath = (0, import_node_path11.join)(sageDir, EXTENDED_INFO_FILENAME);
16406
16636
  const cached = cache.get(filePath);
16407
16637
  if (cached)
16408
16638
  return cached.value;
@@ -16413,7 +16643,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
16413
16643
  async function loadExtendedInfoUncached(filePath, logger) {
16414
16644
  let size;
16415
16645
  try {
16416
- const info = await (0, import_promises3.stat)(filePath);
16646
+ const info = await (0, import_promises4.stat)(filePath);
16417
16647
  if (!info.isFile()) {
16418
16648
  logger.debug(`extended-info: not a regular file at ${filePath}`);
16419
16649
  return null;
@@ -16501,13 +16731,13 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16501
16731
 
16502
16732
  // ../core/dist/installation-id.js
16503
16733
  var import_node_crypto4 = require("node:crypto");
16504
- var import_promises4 = require("node:fs/promises");
16505
- var import_node_path11 = require("node:path");
16734
+ var import_promises5 = require("node:fs/promises");
16735
+ var import_node_path12 = require("node:path");
16506
16736
  init_config();
16507
16737
  init_file_utils();
16508
16738
  async function getInstallationId(sageDirPath) {
16509
16739
  const sageDir = sageDirPath ?? resolvePath("~/.sage");
16510
- const idPath = (0, import_node_path11.join)(sageDir, "installation-id");
16740
+ const idPath = (0, import_node_path12.join)(sageDir, "installation-id");
16511
16741
  let fileExists = false;
16512
16742
  try {
16513
16743
  const existing = await getFileContent(idPath, "utf-8");
@@ -16519,8 +16749,8 @@ async function getInstallationId(sageDirPath) {
16519
16749
  }
16520
16750
  try {
16521
16751
  const id = (0, import_node_crypto4.randomUUID)();
16522
- await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
16523
- await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16752
+ await (0, import_promises5.mkdir)(sageDir, { recursive: true, mode: 448 });
16753
+ await (0, import_promises5.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16524
16754
  return id;
16525
16755
  } catch (err) {
16526
16756
  if (err.code === "EEXIST") {
@@ -16549,10 +16779,10 @@ function resolveTimeoutMs() {
16549
16779
  const ms = Math.floor(parsed * 1e3);
16550
16780
  return Math.min(ms, MAX_EFFECTIVE_TIMEOUT_MS);
16551
16781
  }
16552
- async function sendCommunityIqDetection(args) {
16782
+ async function sendCommunityIqTelemetry(args) {
16553
16783
  const logger = args.logger ?? nullLogger;
16554
16784
  if (!args.communityIqEnabled) {
16555
- logger.debug("Community IQ disabled, skipping detection telemetry");
16785
+ logger.debug("Community IQ disabled, skipping telemetry");
16556
16786
  return;
16557
16787
  }
16558
16788
  let iid;
@@ -16561,15 +16791,17 @@ async function sendCommunityIqDetection(args) {
16561
16791
  } catch {
16562
16792
  }
16563
16793
  if (!iid) {
16564
- logger.debug("Skipping detection telemetry: missing installation id");
16794
+ logger.debug("Skipping telemetry: missing installation id");
16565
16795
  return;
16566
16796
  }
16567
16797
  const envelope = buildSageProxyEnvelope({
16568
16798
  iid,
16569
16799
  versionApp: VERSION,
16570
16800
  agentRuntime: args.agentRuntime ?? "unknown",
16571
- agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown"
16801
+ agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown",
16802
+ config: args.config
16572
16803
  });
16804
+ const blocking = args.blocking ?? true;
16573
16805
  const payload = {
16574
16806
  ...envelope,
16575
16807
  block_event: {
@@ -16578,11 +16810,12 @@ async function sendCommunityIqDetection(args) {
16578
16810
  // canonicalize before calling `evaluateToolCall`, so no further
16579
16811
  // mapping is needed here.
16580
16812
  tool_type: args.toolName,
16581
- verdict: "deny",
16582
- user_action: "blocked",
16813
+ verdict: blocking ? "deny" : "suspicious",
16814
+ ...blocking ? { user_action: "blocked" } : {},
16583
16815
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16584
16816
  ...args.signals && Object.keys(args.signals).length > 0 ? { signals: args.signals } : {},
16585
- content: args.content ?? {}
16817
+ content: args.content ?? {},
16818
+ ...args.contentSnippet ? { content_snippet: args.contentSnippet } : {}
16586
16819
  },
16587
16820
  event_id: args.eventId,
16588
16821
  comment: ""
@@ -16590,20 +16823,21 @@ async function sendCommunityIqDetection(args) {
16590
16823
  const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
16591
16824
  const enriched = mergeExtendedInfo(payload, extendedInfo);
16592
16825
  const timeoutMs = resolveTimeoutMs();
16826
+ const endpoint = blocking ? "/v2/detection" : "/v2/heuristic";
16593
16827
  try {
16594
- const response = await fetch(resolveEndpoint("/v2/detection"), {
16828
+ const response = await fetch(resolveEndpoint(endpoint), {
16595
16829
  method: "POST",
16596
16830
  headers: { "Content-Type": "application/json" },
16597
16831
  body: JSON.stringify(enriched),
16598
16832
  signal: AbortSignal.timeout(timeoutMs)
16599
16833
  });
16600
16834
  if (!response.ok) {
16601
- logger.warn("Detection telemetry send failed", {
16835
+ logger.warn("Community IQ telemetry send failed", {
16602
16836
  eventId: args.eventId,
16603
16837
  status: response.status
16604
16838
  });
16605
16839
  } else {
16606
- logger.debug("Detection telemetry sent", {
16840
+ logger.debug("Community IQ telemetry sent", {
16607
16841
  eventId: args.eventId,
16608
16842
  toolName: args.toolName,
16609
16843
  hookType: args.hookType ?? "PreToolUse"
@@ -16617,6 +16851,29 @@ async function sendCommunityIqDetection(args) {
16617
16851
  }
16618
16852
  }
16619
16853
 
16854
+ // ../core/dist/e2e-capture.js
16855
+ var import_node_path13 = require("node:path");
16856
+ var CAPTURE_MAX_BYTES = 10 * 1024 * 1024;
16857
+ var CAPTURE_MAX_FILES = 2;
16858
+ function captureEnabled() {
16859
+ return !!process.env.SAGE_E2E_CAPTURE_DIR;
16860
+ }
16861
+ async function captureHookInput(event, raw, normalized, options = {}) {
16862
+ const dir = process.env.SAGE_E2E_CAPTURE_DIR;
16863
+ if (!dir)
16864
+ return;
16865
+ try {
16866
+ const prefix = options.filePrefix ?? "";
16867
+ const base = event === "PreToolUse" ? "pre-tool-use.jsonl" : "post-tool-use.jsonl";
16868
+ await appendJsonlEntry({
16869
+ path: (0, import_node_path13.join)(dir, `${prefix}${base}`),
16870
+ max_bytes: CAPTURE_MAX_BYTES,
16871
+ max_files: CAPTURE_MAX_FILES
16872
+ }, { event, raw, normalized });
16873
+ } catch {
16874
+ }
16875
+ }
16876
+
16620
16877
  // ../core/dist/policy.js
16621
16878
  init_types2();
16622
16879
  var SENSITIVITY_POLICY = {
@@ -16840,27 +17097,27 @@ init_config();
16840
17097
 
16841
17098
  // ../core/dist/exceptions.js
16842
17099
  var import_node_crypto5 = require("node:crypto");
16843
- var import_node_path13 = require("node:path");
17100
+ var import_node_path15 = require("node:path");
16844
17101
  init_config();
16845
17102
  init_file_utils();
16846
17103
 
16847
17104
  // ../core/dist/trusted-domains.js
16848
- var import_promises5 = require("node:fs/promises");
16849
- var import_node_path12 = require("node:path");
17105
+ var import_promises6 = require("node:fs/promises");
17106
+ var import_node_path14 = require("node:path");
16850
17107
  var import_yaml = __toESM(require_dist(), 1);
16851
17108
  init_file_utils();
16852
17109
  init_types2();
16853
17110
  async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
16854
17111
  let files;
16855
17112
  try {
16856
- files = (await (0, import_promises5.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
17113
+ files = (await (0, import_promises6.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
16857
17114
  } catch {
16858
17115
  logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
16859
17116
  return [];
16860
17117
  }
16861
17118
  const domains = [];
16862
17119
  for (const filename of files) {
16863
- const filePath = (0, import_node_path12.join)(trustedDomainsDir, filename);
17120
+ const filePath = (0, import_node_path14.join)(trustedDomainsDir, filename);
16864
17121
  let content;
16865
17122
  try {
16866
17123
  content = await getFileContent(filePath);
@@ -17136,7 +17393,7 @@ function matchesDomain(pattern, url) {
17136
17393
  function normalizePatternPath(pattern) {
17137
17394
  const home = getHomeDir();
17138
17395
  const expanded = pattern.startsWith("~/") || pattern === "~" ? `${home}${pattern.slice(1)}` : pattern;
17139
- return (0, import_node_path13.normalize)(expanded);
17396
+ return (0, import_node_path15.normalize)(expanded);
17140
17397
  }
17141
17398
  function matchesPath(pattern, filePath) {
17142
17399
  const hasWildcard = pattern.includes("*");
@@ -17147,7 +17404,7 @@ function matchesPath(pattern, filePath) {
17147
17404
  const normalizedPath = normalizePatternPath(filePath);
17148
17405
  if (normalizedPath === normalizedPattern)
17149
17406
  return true;
17150
- if (normalizedPath.startsWith(normalizedPattern + import_node_path13.sep))
17407
+ if (normalizedPath.startsWith(normalizedPattern + import_node_path15.sep))
17151
17408
  return true;
17152
17409
  return false;
17153
17410
  }
@@ -17463,6 +17720,7 @@ var PackageChecker = class {
17463
17720
  if (pkg.name.startsWith("@")) {
17464
17721
  results.push({
17465
17722
  packageName: pkg.name,
17723
+ packageVersion: pkg.version,
17466
17724
  registry: pkg.registry,
17467
17725
  verdict: "clean",
17468
17726
  confidence: 1,
@@ -17483,6 +17741,7 @@ var PackageChecker = class {
17483
17741
  } catch {
17484
17742
  return {
17485
17743
  packageName: pkg.name,
17744
+ packageVersion: pkg.version,
17486
17745
  registry: pkg.registry,
17487
17746
  verdict: "unknown",
17488
17747
  confidence: 0.6,
@@ -17492,6 +17751,7 @@ var PackageChecker = class {
17492
17751
  if (metadata === null) {
17493
17752
  return {
17494
17753
  packageName: pkg.name,
17754
+ packageVersion: pkg.version,
17495
17755
  registry: pkg.registry,
17496
17756
  verdict: "not_found",
17497
17757
  confidence: 0.95,
@@ -17501,6 +17761,7 @@ var PackageChecker = class {
17501
17761
  if (!metadata.requestedVersionFound && pkg.version) {
17502
17762
  return {
17503
17763
  packageName: pkg.name,
17764
+ packageVersion: pkg.version,
17504
17765
  registry: pkg.registry,
17505
17766
  verdict: "not_found",
17506
17767
  confidence: 0.95,
@@ -17516,6 +17777,7 @@ var PackageChecker = class {
17516
17777
  const detections = fileResult.detectionNames.length > 0 ? fileResult.detectionNames.join(", ") : sev;
17517
17778
  return {
17518
17779
  packageName: pkg.name,
17780
+ packageVersion: pkg.version,
17519
17781
  registry: pkg.registry,
17520
17782
  verdict: "malicious",
17521
17783
  confidence: 1,
@@ -17534,6 +17796,7 @@ var PackageChecker = class {
17534
17796
  if (ageDays !== void 0 && ageDays < SUSPICIOUS_AGE_DAYS) {
17535
17797
  return {
17536
17798
  packageName: pkg.name,
17799
+ packageVersion: pkg.version,
17537
17800
  registry: pkg.registry,
17538
17801
  verdict: "suspicious_age",
17539
17802
  confidence: 0.6,
@@ -17543,6 +17806,7 @@ var PackageChecker = class {
17543
17806
  }
17544
17807
  return {
17545
17808
  packageName: pkg.name,
17809
+ packageVersion: pkg.version,
17546
17810
  registry: pkg.registry,
17547
17811
  verdict: "clean",
17548
17812
  confidence: 1,
@@ -17927,7 +18191,7 @@ function extractFromRequirementsTxt(content) {
17927
18191
  }
17928
18192
 
17929
18193
  // ../core/dist/statusline.js
17930
- var import_node_path14 = require("node:path");
18194
+ var import_node_path16 = require("node:path");
17931
18195
  init_config();
17932
18196
  init_file_utils();
17933
18197
  var STATUS_PREFIX = "statusline-";
@@ -17936,15 +18200,17 @@ function sanitizeSessionId(sessionId) {
17936
18200
  return sessionId.replace(/[^a-zA-Z0-9-]/g, "_");
17937
18201
  }
17938
18202
  function statusFilePath(sessionId) {
17939
- return (0, import_node_path14.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
18203
+ return (0, import_node_path16.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
17940
18204
  }
17941
18205
  function emptyStatus() {
18206
+ const now = (/* @__PURE__ */ new Date()).toISOString();
17942
18207
  return {
17943
18208
  denied: 0,
17944
18209
  flagged: 0,
17945
18210
  lastCategory: null,
17946
18211
  lastReason: null,
17947
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18212
+ updatedAt: now,
18213
+ startedAt: now
17948
18214
  };
17949
18215
  }
17950
18216
  async function readStatus(sessionId) {
@@ -17969,13 +18235,15 @@ async function updateSessionStatus(sessionId, verdict) {
17969
18235
  }
17970
18236
 
17971
18237
  // ../core/dist/threat-loader.js
17972
- var import_promises6 = require("node:fs/promises");
17973
- var import_node_path15 = require("node:path");
18238
+ var import_promises7 = require("node:fs/promises");
18239
+ var import_node_path17 = require("node:path");
17974
18240
  var import_yaml2 = __toESM(require_dist(), 1);
17975
18241
  init_file_utils();
17976
18242
  init_types2();
17977
18243
  var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
17978
18244
  "id",
18245
+ "version",
18246
+ "detection_name",
17979
18247
  "category",
17980
18248
  "severity",
17981
18249
  "confidence",
@@ -17983,6 +18251,37 @@ var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
17983
18251
  "match_on",
17984
18252
  "title"
17985
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
+ }
17986
18285
  function parseExpiresAt(value) {
17987
18286
  if (value == null)
17988
18287
  return null;
@@ -18003,16 +18302,17 @@ async function loadThreats(threatDir, logger = nullLogger) {
18003
18302
  const threats = [];
18004
18303
  let files;
18005
18304
  try {
18006
- files = (await (0, import_promises6.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
18305
+ files = (await (0, import_promises7.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
18007
18306
  } catch {
18008
18307
  logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
18009
18308
  return threats;
18010
18309
  }
18310
+ const ruleFiles = [];
18311
+ let macros = {};
18011
18312
  for (const filename of files) {
18012
- const filePath = (0, import_node_path15.join)(threatDir, filename);
18013
18313
  let content;
18014
18314
  try {
18015
- content = await getFileContent(filePath);
18315
+ content = await getFileContent((0, import_node_path17.join)(threatDir, filename));
18016
18316
  } catch (e) {
18017
18317
  logger.warn(`Failed to read ${filename}`, { error: String(e) });
18018
18318
  continue;
@@ -18024,10 +18324,21 @@ async function loadThreats(threatDir, logger = nullLogger) {
18024
18324
  logger.warn(`Failed to parse ${filename}`, { error: String(e) });
18025
18325
  continue;
18026
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
+ }
18027
18335
  if (!Array.isArray(data)) {
18028
18336
  logger.warn(`Expected list in ${filename}, got ${typeof data}`);
18029
18337
  continue;
18030
18338
  }
18339
+ ruleFiles.push({ filename, data });
18340
+ }
18341
+ for (const { filename, data } of ruleFiles) {
18031
18342
  for (const entry of data) {
18032
18343
  if (typeof entry !== "object" || entry === null) {
18033
18344
  logger.warn(`Skipping non-object entry in ${filename}`);
@@ -18044,10 +18355,12 @@ async function loadThreats(threatDir, logger = nullLogger) {
18044
18355
  continue;
18045
18356
  if (isExpired(record))
18046
18357
  continue;
18358
+ let pattern;
18047
18359
  let compiledPattern;
18048
18360
  try {
18361
+ pattern = expandMacros(record.pattern, macros);
18049
18362
  const flags2 = record.case_insensitive === true ? "i" : "";
18050
- compiledPattern = new RegExp(record.pattern, flags2);
18363
+ compiledPattern = new RegExp(pattern, flags2);
18051
18364
  } catch (e) {
18052
18365
  logger.warn(`Skipping threat ${record.id}: invalid regex pattern`, {
18053
18366
  error: String(e)
@@ -18065,13 +18378,28 @@ async function loadThreats(threatDir, logger = nullLogger) {
18065
18378
  });
18066
18379
  continue;
18067
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
+ }
18068
18395
  threats.push({
18069
18396
  id: record.id,
18070
- version: typeof record.version === "number" ? record.version : void 0,
18397
+ version,
18398
+ detectionName,
18071
18399
  category: record.category,
18072
18400
  severity: record.severity,
18073
18401
  confidence,
18074
- pattern: record.pattern,
18402
+ pattern,
18075
18403
  compiledPattern,
18076
18404
  matchOn,
18077
18405
  title: record.title,
@@ -18089,6 +18417,29 @@ async function loadThreats(threatDir, logger = nullLogger) {
18089
18417
  init_types2();
18090
18418
  var AMSI_CONTENT_SNIPPET_MAX = 200;
18091
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
+ }
18092
18443
  function scrubAmsiContentName(contentName) {
18093
18444
  const colon = contentName.indexOf(":");
18094
18445
  if (colon < 0)
@@ -18098,16 +18449,10 @@ function scrubAmsiContentName(contentName) {
18098
18449
  return `${head}${scrubHomePath(tail)}`;
18099
18450
  }
18100
18451
  function buildAmsiSignal(r) {
18101
- const detectionName = r.amsiResult >= 32768 ? "AMSI|DETECTED" : r.amsiResult >= 16384 ? "AMSI|BLOCKED_BY_ADMIN" : (
18102
- // Defensive: callers should filter these out via `isDetected || isBlockedByAdmin`,
18103
- // but if a non-detected/non-blocked result still reaches here we emit a
18104
- // meaningful label rather than silently dropping the entry.
18105
- "AMSI|UNKNOWN"
18106
- );
18107
18452
  const contentName = safeTruncate(scrubAmsiContentName(r.contentName), AMSI_CONTENT_NAME_MAX);
18108
18453
  const snippet = r.content ? safeTruncate(scrubHomePath(r.content), AMSI_CONTENT_SNIPPET_MAX) : "";
18109
18454
  return {
18110
- detection_name: detectionName,
18455
+ detection_name: formatReportingDetectionName(amsiDetectionName(r.amsiResult), "amsi", amsiReportingData(r.amsiResult)),
18111
18456
  content_name: contentName,
18112
18457
  amsi_result: r.amsiResult,
18113
18458
  ...snippet ? { content_snippet: snippet } : {}
@@ -18179,7 +18524,7 @@ async function evaluateToolCall(request, context) {
18179
18524
  severity: "critical",
18180
18525
  source: "exception",
18181
18526
  artifacts: [denyMatch.artifact.value],
18182
- matchedThreatId: null,
18527
+ matchedThreatId: denyMatch.rule.id,
18183
18528
  reasons: [
18184
18529
  `Deny exception: ${denyMatch.rule.match} pattern '${denyMatch.rule.pattern}'${denyMatch.rule.reason ? ` \u2014 ${denyMatch.rule.reason}` : ""}`
18185
18530
  ]
@@ -18349,6 +18694,12 @@ async function evaluateToolCall(request, context) {
18349
18694
  if (result) {
18350
18695
  allPiResults.push(result);
18351
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
+ });
18352
18703
  }
18353
18704
  }
18354
18705
  }
@@ -18365,8 +18716,8 @@ async function evaluateToolCall(request, context) {
18365
18716
  amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
18366
18717
  piCheckResults: allPiResults,
18367
18718
  piThresholds: {
18368
- highRisk: config2.pi_check.high_risk_threshold,
18369
- mediumRisk: config2.pi_check.medium_risk_threshold
18719
+ highRisk: DEFAULT_PI_HIGH_RISK_THRESHOLD,
18720
+ mediumRisk: DEFAULT_PI_MEDIUM_RISK_THRESHOLD
18370
18721
  }
18371
18722
  });
18372
18723
  } else {
@@ -18394,24 +18745,9 @@ async function evaluateToolCall(request, context) {
18394
18745
  }
18395
18746
  }
18396
18747
  await cacheUrlResults(urlCheckResults, cache2);
18397
- function formatPackageDetectionName(p) {
18398
- const base = `PKG|${p.verdict}|registry=${p.registry}|name=${p.packageName}`;
18399
- if (p.verdict === "suspicious_age") {
18400
- const ageDays = typeof p.ageDays === "number" ? Math.floor(p.ageDays) : void 0;
18401
- return ageDays !== void 0 ? `${base}|age_days=${ageDays}` : base;
18402
- }
18403
- if (p.verdict === "malicious") {
18404
- const det = (p.fileDetectionNames ?? []).filter((d) => typeof d === "string" && d.length > 0);
18405
- return det.length > 0 ? `${base}|det=${det.join(",")}` : base;
18406
- }
18407
- return base;
18408
- }
18409
18748
  const auditSignals = {};
18410
18749
  if (heuristicMatches.length > 0) {
18411
- auditSignals.heuristics = heuristicMatches.map((m) => ({
18412
- rule_id: m.threat.id,
18413
- rule_version: typeof m.threat.version === "number" ? m.threat.version : void 0
18414
- }));
18750
+ auditSignals.heuristics = heuristicMatches.map(buildHeuristicSignal);
18415
18751
  }
18416
18752
  if (urlCheckResults.length > 0) {
18417
18753
  const relevant = urlCheckResults.filter((r) => r.isMalicious);
@@ -18442,9 +18778,9 @@ async function evaluateToolCall(request, context) {
18442
18778
  const relevant = packageCheckResults.filter((p) => p.verdict !== "clean");
18443
18779
  if (relevant.length > 0) {
18444
18780
  auditSignals.package_checks = relevant.map((p) => ({
18445
- detection_name: formatPackageDetectionName(p),
18781
+ detection_name: formatReportingDetectionName(packageDetectionName(p.verdict), "package", p.packageVersion ? `${p.packageName}:${p.packageVersion}` : p.packageName),
18446
18782
  package_name: p.packageName,
18447
- package_version: void 0,
18783
+ package_version: p.packageVersion,
18448
18784
  package_registry: p.registry
18449
18785
  }));
18450
18786
  }
@@ -18457,8 +18793,9 @@ async function evaluateToolCall(request, context) {
18457
18793
  }
18458
18794
  }
18459
18795
  if (allPiResults.length > 0) {
18460
- const piSnippetFloor = config2.pi_check.medium_risk_threshold;
18796
+ const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
18461
18797
  auditSignals.pi_checks = allPiResults.map((r) => ({
18798
+ detection_name: formatReportingDetectionName(PI_DETECTION_NAME, "ml", `${r.modelId}:${MODEL_SCHEMA_VERSION}`),
18462
18799
  risk: r.risk,
18463
18800
  model_id: r.modelId,
18464
18801
  content_name: r.contentName,
@@ -18472,6 +18809,7 @@ async function evaluateToolCall(request, context) {
18472
18809
  }
18473
18810
  }
18474
18811
  const resolvedSignals = Object.keys(auditSignals).length > 0 ? auditSignals : void 0;
18812
+ const contentSnippet = resolvePiContentSnippet(allPiResults, heuristicMatches);
18475
18813
  const builtContent = buildContentSnapshot(request.toolName, request.toolInput, request.artifacts, auditSignals);
18476
18814
  const resolvedContent = Object.keys(builtContent).length > 0 ? builtContent : void 0;
18477
18815
  try {
@@ -18485,28 +18823,37 @@ async function evaluateToolCall(request, context) {
18485
18823
  hookType: request.hookType,
18486
18824
  signals: resolvedSignals,
18487
18825
  content: resolvedContent,
18826
+ contentSnippet,
18488
18827
  eventId,
18489
18828
  toolUseId: request.toolUseId
18490
18829
  });
18491
18830
  } catch (error) {
18492
18831
  logger.debug("Audit verdict logging failed open", { error: String(error) });
18493
18832
  }
18494
- if (verdict.decision === "deny") {
18833
+ const submitTelemetry = async (blocking) => {
18495
18834
  try {
18496
- await sendCommunityIqDetection({
18835
+ await sendCommunityIqTelemetry({
18497
18836
  eventId,
18498
18837
  agentRuntime: request.agentRuntime,
18499
18838
  agentRuntimeVersion: request.agentRuntimeVersion,
18500
18839
  hookType: request.hookType,
18501
18840
  toolName: request.toolName,
18502
18841
  content: resolvedContent,
18842
+ contentSnippet,
18503
18843
  signals: resolvedSignals,
18504
18844
  communityIqEnabled: config2.community_iq,
18845
+ config: config2,
18846
+ blocking,
18505
18847
  logger
18506
18848
  });
18507
18849
  } catch (error) {
18508
- logger.debug("Detection telemetry failed open", { error: String(error) });
18850
+ logger.debug("Community IQ telemetry failed open", { error: String(error) });
18509
18851
  }
18852
+ };
18853
+ if (verdict.decision === "deny") {
18854
+ await submitTelemetry(true);
18855
+ } else if (allPiResults.some((r) => r.risk >= DEFAULT_PI_TELEMETRY_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD)) {
18856
+ await submitTelemetry(false);
18510
18857
  }
18511
18858
  if (verdict.decision !== "allow") {
18512
18859
  try {
@@ -18515,7 +18862,7 @@ async function evaluateToolCall(request, context) {
18515
18862
  logger.debug("Session status update failed open", { error: String(error) });
18516
18863
  }
18517
18864
  }
18518
- const piWarnings = allPiResults.filter((r) => r.risk >= config2.pi_check.medium_risk_threshold && r.risk < config2.pi_check.high_risk_threshold);
18865
+ const piWarnings = allPiResults.filter((r) => r.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD);
18519
18866
  if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
18520
18867
  verdict.piWarnings = piWarnings;
18521
18868
  logEvaluationCompleted(verdict);
@@ -18618,6 +18965,7 @@ async function checkPackages(request, config2, cache2, logger) {
18618
18965
  }
18619
18966
  results.push({
18620
18967
  packageName: pkg.name,
18968
+ packageVersion: pkg.version,
18621
18969
  registry: pkg.registry,
18622
18970
  verdict: rawVerdict,
18623
18971
  confidence: rawConf,
@@ -18657,7 +19005,7 @@ function createPiProvider(config2, _context, logger) {
18657
19005
  return new BundledPiProvider({
18658
19006
  modelPath: config2.pi_check.model_path,
18659
19007
  maxContentLength: config2.pi_check.max_content_length,
18660
- mediumRiskThreshold: config2.pi_check.medium_risk_threshold,
19008
+ mediumRiskThreshold: DEFAULT_PI_MEDIUM_RISK_THRESHOLD,
18661
19009
  logger
18662
19010
  });
18663
19011
  }
@@ -18682,6 +19030,19 @@ function kv(key, value) {
18682
19030
  function separatorLine(headerLength) {
18683
19031
  return "\u2501".repeat(headerLength);
18684
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
+ }
18685
19046
  function formatUpdateNotice(result) {
18686
19047
  return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/gendigitalinc/sage)`;
18687
19048
  }
@@ -18828,15 +19189,225 @@ function summarizeArtifacts(artifacts) {
18828
19189
  function formatDenyMessage(verdict, branding2 = defaultBranding) {
18829
19190
  const maxReasons = 5;
18830
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);
18831
19193
  return [
18832
19194
  `${branding2.name} blocked this action.`,
18833
19195
  `Severity: ${verdict.severity}`,
18834
19196
  `Category: ${verdict.category}`,
19197
+ ...rule ? [`Rule: ${rule}`] : [],
18835
19198
  `Reason: ${reasons}`,
18836
- `If this is a false positive, use the sage_report_false_positive MCP tool to report it.`
19199
+ remediationHint(verdict)
18837
19200
  ].join("\n");
18838
19201
  }
18839
19202
 
19203
+ // ../core/dist/install-state.js
19204
+ var import_node_path18 = require("node:path");
19205
+ init_config();
19206
+ init_file_utils();
19207
+
19208
+ // ../core/dist/notices.js
19209
+ init_config();
19210
+ var SKILL_UPLOAD_NOTICE = {
19211
+ id: "skill_upload_v1",
19212
+ body: () => [
19213
+ "unknown skills \u2014 each skill's SKILL.md and every supporting file in its",
19214
+ "folder \u2014 will be uploaded and checked against potential malicious artifacts",
19215
+ "starting next session. To keep skill content on this machine, set",
19216
+ '"skill_check": { "upload_enabled": false } in ~/.sage/config.json.'
19217
+ ],
19218
+ // Suppressed once the user has explicitly set upload_enabled either way: an
19219
+ // explicit choice needs no consent prompt (the rollout obeys it directly).
19220
+ isEligible: async ({ configPath, logger }) => !(await readExplicitSkillUploadEnabled(configPath, logger)).present
19221
+ };
19222
+ var NOTICES = {
19223
+ [SKILL_UPLOAD_NOTICE.id]: SKILL_UPLOAD_NOTICE
19224
+ };
19225
+ var INFO_ICON = "\u2139\uFE0F";
19226
+ var CONTINUATION_INDENT = " ";
19227
+ function formatNotice(notice, branding2 = defaultBranding) {
19228
+ const [first = "", ...rest] = notice.body(branding2);
19229
+ const head = `${INFO_ICON} ${branding2.name}: ${first}`;
19230
+ return [head, ...rest.map((line) => `${CONTINUATION_INDENT}${line}`)].join("\n");
19231
+ }
19232
+ function formatNoticeById(id, branding2 = defaultBranding) {
19233
+ const notice = NOTICES[id];
19234
+ return notice ? formatNotice(notice, branding2) : "";
19235
+ }
19236
+
19237
+ // ../core/dist/install-state.js
19238
+ init_types2();
19239
+ var INSTALL_STATE_SCHEMA_VERSION = 1;
19240
+ var SKILL_UPLOAD_NOTICE_ID = SKILL_UPLOAD_NOTICE.id;
19241
+ function defaultStatePath(sageDirPath) {
19242
+ return (0, import_node_path18.join)(sageDirPath ?? resolvePath(SAGE_DIR), "install-state.json");
19243
+ }
19244
+ function emptyState() {
19245
+ return { schemaVersion: INSTALL_STATE_SCHEMA_VERSION, notices: {} };
19246
+ }
19247
+ async function loadInstallState(sageDirPath, logger = nullLogger) {
19248
+ let raw;
19249
+ try {
19250
+ raw = await getFileContent(defaultStatePath(sageDirPath));
19251
+ } catch {
19252
+ return emptyState();
19253
+ }
19254
+ try {
19255
+ const data = JSON.parse(raw);
19256
+ const rawNotices = data.notices ?? {};
19257
+ const notices = {};
19258
+ for (const [id, rec] of Object.entries(rawNotices)) {
19259
+ notices[id] = {
19260
+ noticed: rec.noticed === true,
19261
+ version: typeof rec.version === "string" ? rec.version : void 0
19262
+ };
19263
+ }
19264
+ return {
19265
+ schemaVersion: typeof data.schema_version === "number" ? data.schema_version : INSTALL_STATE_SCHEMA_VERSION,
19266
+ lastRunVersion: typeof data.last_run_version === "string" ? data.last_run_version : void 0,
19267
+ notices
19268
+ };
19269
+ } catch (e) {
19270
+ logger.warn("Failed to parse install state; treating as fresh", { error: String(e) });
19271
+ return emptyState();
19272
+ }
19273
+ }
19274
+ async function saveInstallState(state, sageDirPath, logger = nullLogger) {
19275
+ try {
19276
+ const data = {
19277
+ schema_version: state.schemaVersion,
19278
+ last_run_version: state.lastRunVersion,
19279
+ notices: Object.fromEntries(Object.entries(state.notices).map(([id, rec]) => [
19280
+ id,
19281
+ { noticed: rec.noticed, version: rec.version }
19282
+ ]))
19283
+ };
19284
+ await atomicWriteJson(defaultStatePath(sageDirPath), data);
19285
+ } catch (e) {
19286
+ logger.warn("Failed to save install state", { error: String(e) });
19287
+ }
19288
+ }
19289
+ function decideSkillUploadRollout(state, opts = {}) {
19290
+ const versionChanged = opts.version !== void 0 && state.lastRunVersion !== opts.version;
19291
+ const withVersion = () => ({ ...state, lastRunVersion: opts.version });
19292
+ if (opts.explicitConfig) {
19293
+ return {
19294
+ uploadActive: opts.explicitValue === true,
19295
+ showNotice: false,
19296
+ nextState: versionChanged ? withVersion() : null
19297
+ };
19298
+ }
19299
+ const notice = state.notices[SKILL_UPLOAD_NOTICE_ID];
19300
+ if (notice?.noticed === true) {
19301
+ return {
19302
+ uploadActive: true,
19303
+ showNotice: false,
19304
+ nextState: versionChanged ? withVersion() : null
19305
+ };
19306
+ }
19307
+ const nextState = {
19308
+ ...state,
19309
+ lastRunVersion: opts.version ?? state.lastRunVersion,
19310
+ notices: {
19311
+ ...state.notices,
19312
+ [SKILL_UPLOAD_NOTICE_ID]: { noticed: true, version: opts.version }
19313
+ }
19314
+ };
19315
+ return { uploadActive: false, showNotice: true, nextState };
19316
+ }
19317
+ var processRollout;
19318
+ async function resolveSkillUploadRollout(args) {
19319
+ const commitNotice = args.commitNotice ?? true;
19320
+ const logger = args.logger ?? nullLogger;
19321
+ let explicit;
19322
+ try {
19323
+ explicit = await readExplicitSkillUploadEnabled(args.configPath, logger);
19324
+ } catch {
19325
+ explicit = { present: false, value: false };
19326
+ }
19327
+ if (explicit.present) {
19328
+ const resolved = {
19329
+ uploadActive: explicit.value === true,
19330
+ showNotice: false,
19331
+ deferUnknownSkills: false
19332
+ };
19333
+ logger.debug("Skill-upload rollout resolved", {
19334
+ explicitConfig: true,
19335
+ explicitValue: explicit.value,
19336
+ uploadActive: resolved.uploadActive,
19337
+ showNotice: false,
19338
+ deferUnknownSkills: false,
19339
+ commitNotice,
19340
+ statePersisted: false,
19341
+ version: args.version
19342
+ });
19343
+ return resolved;
19344
+ }
19345
+ if (processRollout)
19346
+ return processRollout;
19347
+ try {
19348
+ const state = await loadInstallState(args.sageDirPath, logger);
19349
+ const decision = decideSkillUploadRollout(state, { version: args.version });
19350
+ if (commitNotice) {
19351
+ if (decision.nextState) {
19352
+ await saveInstallState(decision.nextState, args.sageDirPath, logger);
19353
+ }
19354
+ processRollout = {
19355
+ uploadActive: decision.uploadActive,
19356
+ showNotice: decision.showNotice,
19357
+ deferUnknownSkills: decision.showNotice
19358
+ };
19359
+ } else {
19360
+ processRollout = {
19361
+ uploadActive: decision.uploadActive,
19362
+ showNotice: false,
19363
+ deferUnknownSkills: decision.showNotice
19364
+ };
19365
+ }
19366
+ logger.debug("Skill-upload rollout resolved", {
19367
+ explicitConfig: false,
19368
+ uploadActive: processRollout.uploadActive,
19369
+ showNotice: processRollout.showNotice,
19370
+ deferUnknownSkills: processRollout.deferUnknownSkills,
19371
+ commitNotice,
19372
+ statePersisted: commitNotice && decision.nextState !== null,
19373
+ version: args.version
19374
+ });
19375
+ } catch (e) {
19376
+ logger.warn("Skill-upload rollout resolution failed; suppressing upload", {
19377
+ error: String(e)
19378
+ });
19379
+ processRollout = { uploadActive: false, showNotice: commitNotice, deferUnknownSkills: true };
19380
+ }
19381
+ return processRollout;
19382
+ }
19383
+ async function takePendingNotices(args) {
19384
+ const logger = args.logger ?? nullLogger;
19385
+ try {
19386
+ const state = await loadInstallState(args.sageDirPath, logger);
19387
+ const toShow = [];
19388
+ for (const notice of Object.values(NOTICES)) {
19389
+ if (state.notices[notice.id]?.noticed === true)
19390
+ continue;
19391
+ const eligible = notice.isEligible ? await notice.isEligible({ configPath: args.configPath, logger }) : true;
19392
+ if (eligible)
19393
+ toShow.push(notice.id);
19394
+ }
19395
+ if (toShow.length === 0)
19396
+ return [];
19397
+ const nextNotices = { ...state.notices };
19398
+ for (const id of toShow)
19399
+ nextNotices[id] = { noticed: true };
19400
+ await saveInstallState({ ...state, notices: nextNotices }, args.sageDirPath, logger);
19401
+ logger.debug("Pending notices taken for delivery", { noticeIds: toShow });
19402
+ return toShow;
19403
+ } catch (e) {
19404
+ logger.warn("takePendingNotices failed; showing no notices this turn", {
19405
+ error: String(e)
19406
+ });
19407
+ return [];
19408
+ }
19409
+ }
19410
+
18840
19411
  // ../core/dist/model-download.js
18841
19412
  init_types2();
18842
19413
 
@@ -18908,8 +19479,8 @@ function createOperationalLogger(config2, runtime) {
18908
19479
  try {
18909
19480
  const result = await Promise.race([
18910
19481
  Promise.allSettled([...pendingWrites]).then(() => "drained"),
18911
- new Promise((resolve9) => {
18912
- timeout = setTimeout(() => resolve9("timeout"), remainingMs);
19482
+ new Promise((resolve10) => {
19483
+ timeout = setTimeout(() => resolve10("timeout"), remainingMs);
18913
19484
  timeout.unref?.();
18914
19485
  })
18915
19486
  ]);
@@ -18958,13 +19529,14 @@ function createOperationalLogger(config2, runtime) {
18958
19529
 
18959
19530
  // ../core/dist/plugin-scan-cache.js
18960
19531
  var import_node_crypto7 = require("node:crypto");
18961
- var import_promises7 = require("node:fs/promises");
18962
- var import_node_path16 = require("node:path");
19532
+ var import_promises8 = require("node:fs/promises");
19533
+ var import_node_path19 = require("node:path");
19534
+ init_config();
18963
19535
  init_file_utils();
18964
19536
  init_types2();
18965
- var DEFAULT_CACHE_PATH = (0, import_node_path16.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
18966
- var CACHE_TTL_DAYS = 1;
18967
- var SCHEMA_VERSION = 3;
19537
+ var DEFAULT_CACHE_PATH = (0, import_node_path19.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
19538
+ var DEFAULT_CACHE_TTL_MS = MS_PER_DAY;
19539
+ var SCHEMA_VERSION = 5;
18968
19540
  function cacheKey(pluginKey, version, lastUpdated) {
18969
19541
  return `${pluginKey}:${version}:${lastUpdated}`;
18970
19542
  }
@@ -18975,13 +19547,13 @@ async function computeConfigHash(sageVersion, ...dirs) {
18975
19547
  for (const dir of dirs) {
18976
19548
  let files;
18977
19549
  try {
18978
- files = (await (0, import_promises7.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
19550
+ files = (await (0, import_promises8.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
18979
19551
  } catch {
18980
19552
  continue;
18981
19553
  }
18982
19554
  for (const file of files) {
18983
19555
  try {
18984
- const content = await getFileContent((0, import_node_path16.join)(dir, file));
19556
+ const content = await getFileContent((0, import_node_path19.join)(dir, file));
18985
19557
  h.update(content);
18986
19558
  } catch {
18987
19559
  }
@@ -19050,7 +19622,7 @@ async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nu
19050
19622
  logger.warn(`Failed to save scan cache to ${cachePath}`, { error: String(e) });
19051
19623
  }
19052
19624
  }
19053
- function isCached(cache2, pluginKey, version, lastUpdated) {
19625
+ function isCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
19054
19626
  const key = cacheKey(pluginKey, version, lastUpdated);
19055
19627
  const entry = cache2.entries[key];
19056
19628
  if (!entry)
@@ -19058,13 +19630,13 @@ function isCached(cache2, pluginKey, version, lastUpdated) {
19058
19630
  try {
19059
19631
  const scannedAt = new Date(entry.scannedAt);
19060
19632
  const age = Date.now() - scannedAt.getTime();
19061
- return age < CACHE_TTL_DAYS * 24 * 60 * 60 * 1e3;
19633
+ return age < ttlMs;
19062
19634
  } catch {
19063
19635
  return false;
19064
19636
  }
19065
19637
  }
19066
- function getCached(cache2, pluginKey, version, lastUpdated) {
19067
- if (!isCached(cache2, pluginKey, version, lastUpdated))
19638
+ function getCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
19639
+ if (!isCached(cache2, pluginKey, version, lastUpdated, ttlMs))
19068
19640
  return null;
19069
19641
  const key = cacheKey(pluginKey, version, lastUpdated);
19070
19642
  return cache2.entries[key] ?? null;
@@ -19081,18 +19653,21 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
19081
19653
 
19082
19654
  // ../core/dist/plugin-scanner.js
19083
19655
  var import_node_crypto9 = require("node:crypto");
19084
- var import_promises9 = require("node:fs/promises");
19085
- var import_node_path18 = require("node:path");
19656
+ var import_promises11 = require("node:fs/promises");
19657
+ var import_node_path23 = require("node:path");
19086
19658
  init_config();
19087
19659
  init_file_utils();
19088
19660
 
19089
19661
  // ../core/dist/skill-id.js
19090
19662
  var import_node_crypto8 = require("node:crypto");
19091
- var import_promises8 = require("node:fs/promises");
19092
- var import_node_path17 = require("node:path");
19663
+ var import_promises9 = require("node:fs/promises");
19664
+ var import_node_path20 = require("node:path");
19093
19665
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
19666
+ var MAX_SKILL_BYTES = 50 * 1024 * 1024;
19667
+ var SkillTooLargeError = class extends Error {
19668
+ };
19094
19669
  function isContained(childReal, rootReal) {
19095
- return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path17.sep}`);
19670
+ return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path20.sep}`);
19096
19671
  }
19097
19672
  function normalizePath(p) {
19098
19673
  let out = p.replace(/\\/g, "/").normalize("NFC");
@@ -19102,47 +19677,51 @@ function normalizePath(p) {
19102
19677
  }
19103
19678
  return out;
19104
19679
  }
19105
- async function entriesFromDirectory(dirPath) {
19106
- const absDir = (0, import_node_path17.resolve)(dirPath);
19107
- const rootReal = await (0, import_promises8.realpath)(absDir);
19680
+ async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY) {
19681
+ const absDir = (0, import_node_path20.resolve)(dirPath);
19682
+ const rootReal = await (0, import_promises9.realpath)(absDir);
19108
19683
  const entries = [];
19109
19684
  const visited = /* @__PURE__ */ new Set();
19685
+ let total = 0;
19110
19686
  async function walk(currentPath) {
19111
- const real = await (0, import_promises8.realpath)(currentPath);
19687
+ const real = await (0, import_promises9.realpath)(currentPath);
19112
19688
  if (visited.has(real))
19113
19689
  return;
19114
19690
  if (!isContained(real, rootReal))
19115
19691
  return;
19116
19692
  visited.add(real);
19117
- const items = await (0, import_promises8.readdir)(currentPath);
19693
+ const items = await (0, import_promises9.readdir)(currentPath);
19118
19694
  const dirs = [];
19119
19695
  const files = [];
19120
19696
  for (const item of items) {
19121
- const fullPath = (0, import_node_path17.join)(currentPath, item);
19122
- const st = await (0, import_promises8.stat)(fullPath);
19697
+ const fullPath = (0, import_node_path20.join)(currentPath, item);
19698
+ const st = await (0, import_promises9.stat)(fullPath);
19123
19699
  if (st.isDirectory())
19124
19700
  dirs.push(item);
19125
19701
  else if (st.isFile())
19126
- files.push(item);
19702
+ files.push({ name: item, size: st.size });
19127
19703
  }
19128
19704
  dirs.sort();
19129
- files.sort();
19705
+ files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
19130
19706
  for (const d of dirs) {
19131
- const fullPath = (0, import_node_path17.join)(currentPath, d);
19132
- const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
19707
+ const fullPath = (0, import_node_path20.join)(currentPath, d);
19708
+ const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
19133
19709
  entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
19134
19710
  await walk(fullPath);
19135
19711
  }
19136
19712
  for (const f of files) {
19137
- const fullPath = (0, import_node_path17.join)(currentPath, f);
19138
- const fileReal = await (0, import_promises8.realpath)(fullPath);
19713
+ const fullPath = (0, import_node_path20.join)(currentPath, f.name);
19714
+ const fileReal = await (0, import_promises9.realpath)(fullPath);
19139
19715
  if (!isContained(fileReal, rootReal))
19140
19716
  continue;
19141
- const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
19717
+ total += f.size;
19718
+ if (total > maxBytes)
19719
+ throw new SkillTooLargeError();
19720
+ const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
19142
19721
  entries.push({
19143
19722
  entryPath: relPath,
19144
19723
  isDir: false,
19145
- content: await (0, import_promises8.readFile)(fullPath)
19724
+ content: await (0, import_promises9.readFile)(fullPath)
19146
19725
  });
19147
19726
  }
19148
19727
  }
@@ -19179,77 +19758,347 @@ function computeSkillId(entries) {
19179
19758
  }
19180
19759
  return { skillId: skillHasher.digest("hex"), fileHashes };
19181
19760
  }
19182
- async function findSkillPackages(rootDir) {
19761
+ async function findSkillPackagesWithMtime(rootDir) {
19183
19762
  const found = [];
19184
19763
  let rootReal;
19185
19764
  try {
19186
- const rootStat = await (0, import_promises8.stat)(rootDir);
19187
- if (!rootStat.isDirectory())
19188
- return found;
19189
- rootReal = await (0, import_promises8.realpath)((0, import_node_path17.resolve)(rootDir));
19765
+ rootReal = await (0, import_promises9.realpath)((0, import_node_path20.resolve)(rootDir));
19190
19766
  } catch {
19191
19767
  return found;
19192
19768
  }
19193
19769
  const visited = /* @__PURE__ */ new Set();
19194
- async function walk(dir) {
19770
+ async function walk(currentPath) {
19195
19771
  let real;
19196
19772
  try {
19197
- real = await (0, import_promises8.realpath)(dir);
19773
+ real = await (0, import_promises9.realpath)(currentPath);
19198
19774
  } catch {
19199
- return;
19775
+ return 0;
19200
19776
  }
19201
19777
  if (visited.has(real))
19202
- return;
19778
+ return 0;
19203
19779
  if (!isContained(real, rootReal))
19204
- return;
19780
+ return 0;
19205
19781
  visited.add(real);
19782
+ let st;
19783
+ try {
19784
+ st = await (0, import_promises9.stat)(currentPath);
19785
+ } catch {
19786
+ return 0;
19787
+ }
19788
+ let subtreeMax = st.mtimeMs;
19789
+ if (!st.isDirectory())
19790
+ return subtreeMax;
19206
19791
  let items;
19207
19792
  try {
19208
- items = await (0, import_promises8.readdir)(dir);
19793
+ items = await (0, import_promises9.readdir)(currentPath);
19209
19794
  } catch {
19210
- return;
19795
+ return subtreeMax;
19211
19796
  }
19212
19797
  items.sort();
19798
+ let entry;
19213
19799
  if (items.includes("SKILL.md")) {
19214
19800
  try {
19215
- const st = await (0, import_promises8.stat)((0, import_node_path17.join)(dir, "SKILL.md"));
19216
- if (st.isFile())
19217
- found.push(dir);
19801
+ if ((await (0, import_promises9.stat)((0, import_node_path20.join)(currentPath, "SKILL.md"))).isFile()) {
19802
+ entry = { folder: currentPath, newestMtimeMs: 0 };
19803
+ found.push(entry);
19804
+ }
19218
19805
  } catch {
19219
19806
  }
19220
19807
  }
19221
19808
  for (const item of items) {
19222
19809
  if (SKIP_DIRS.has(item))
19223
19810
  continue;
19224
- const fullPath = (0, import_node_path17.join)(dir, item);
19225
- let st;
19226
- try {
19227
- st = await (0, import_promises8.stat)(fullPath);
19228
- } catch {
19229
- continue;
19230
- }
19231
- if (st.isDirectory()) {
19232
- await walk(fullPath);
19233
- }
19811
+ const childMax = await walk((0, import_node_path20.join)(currentPath, item));
19812
+ if (childMax > subtreeMax)
19813
+ subtreeMax = childMax;
19234
19814
  }
19815
+ if (entry)
19816
+ entry.newestMtimeMs = subtreeMax;
19817
+ return subtreeMax;
19235
19818
  }
19236
- await walk((0, import_node_path17.resolve)(rootDir));
19819
+ await walk((0, import_node_path20.resolve)(rootDir));
19237
19820
  return found;
19238
19821
  }
19239
- async function computeSkillIdsForRoot(rootDir) {
19240
- const folders = await findSkillPackages(rootDir);
19822
+ function looseSkillKey(skillsRoot, folder, tag, scope) {
19823
+ const rel = (0, import_node_path20.relative)((0, import_node_path20.resolve)(skillsRoot), (0, import_node_path20.resolve)(folder)).split(import_node_path20.sep).join("/") || (0, import_node_path20.basename)(folder);
19824
+ return `skill:${tag}/${rel}@${scope}`;
19825
+ }
19826
+ async function discoverLooseSkills(skillsDir, tag, scope) {
19827
+ const resolvedRoot = (0, import_node_path20.resolve)(skillsDir);
19828
+ const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0, import_node_path20.resolve)(p.folder) !== resolvedRoot);
19829
+ return packages.map(({ folder, newestMtimeMs }) => ({
19830
+ key: looseSkillKey(resolvedRoot, folder, tag, scope),
19831
+ installPath: folder,
19832
+ version: scope,
19833
+ lastUpdated: new Date(newestMtimeMs).toISOString()
19834
+ }));
19835
+ }
19836
+ async function dedupeSkillsByResolvedPath(skills) {
19837
+ const seen = /* @__PURE__ */ new Set();
19838
+ const out = [];
19839
+ for (const skill of skills) {
19840
+ let canonical = skill.installPath;
19841
+ try {
19842
+ canonical = await (0, import_promises9.realpath)(skill.installPath);
19843
+ } catch {
19844
+ }
19845
+ if (seen.has(canonical))
19846
+ continue;
19847
+ seen.add(canonical);
19848
+ out.push(skill);
19849
+ }
19850
+ return out;
19851
+ }
19852
+ async function discoverLooseSkillsAcrossRoots(roots) {
19853
+ const lists = await Promise.all(roots.map(async ({ dir, tag, scope }) => {
19854
+ try {
19855
+ return await discoverLooseSkills(dir, tag, scope);
19856
+ } catch {
19857
+ return [];
19858
+ }
19859
+ }));
19860
+ return lists.length <= 1 ? lists[0] ?? [] : dedupeSkillsByResolvedPath(lists.flat());
19861
+ }
19862
+ async function computeSkillIdsForRoot(rootDir, logger) {
19863
+ const folders = (await findSkillPackagesWithMtime(rootDir)).map((p) => p.folder);
19241
19864
  const out = [];
19242
19865
  for (const folder of folders) {
19243
19866
  try {
19244
- const entries = await entriesFromDirectory(folder);
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
+ }
19245
19872
  const { skillId } = computeSkillId(entries);
19246
19873
  out.push({ folder, skillId });
19247
- } catch {
19874
+ } catch (e) {
19875
+ if (e instanceof SkillTooLargeError) {
19876
+ logger?.warn("Skill directory exceeds 50 MB limit; skipping", { folder });
19877
+ }
19248
19878
  }
19249
19879
  }
19250
19880
  return out;
19251
19881
  }
19252
19882
 
19883
+ // ../core/dist/skill-pending.js
19884
+ var import_promises10 = require("node:fs/promises");
19885
+ var import_node_path21 = require("node:path");
19886
+ init_config();
19887
+ init_file_utils();
19888
+ init_types2();
19889
+ var SCHEMA_VERSION2 = 1;
19890
+ var PENDING_TTL_MS = 60 * 60 * 1e3;
19891
+ function defaultPendingPath() {
19892
+ return (0, import_node_path21.join)(resolvePath(SAGE_DIR), "skill_pending.json");
19893
+ }
19894
+ async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nullLogger) {
19895
+ let raw;
19896
+ try {
19897
+ raw = await getFileContent(pendingPath);
19898
+ } catch {
19899
+ return { entries: {} };
19900
+ }
19901
+ try {
19902
+ const data = JSON.parse(raw);
19903
+ const rawEntries = data.entries ?? {};
19904
+ const now = Date.now();
19905
+ const entries = {};
19906
+ for (const [skillId, entry] of Object.entries(rawEntries)) {
19907
+ const folder = entry.folder;
19908
+ const submittedAt = entry.submitted_at;
19909
+ if (!folder || !submittedAt)
19910
+ continue;
19911
+ const ts = Date.parse(submittedAt);
19912
+ if (!isFreshTimestamp(ts, PENDING_TTL_MS, now))
19913
+ continue;
19914
+ entries[skillId] = {
19915
+ folder,
19916
+ submittedAt,
19917
+ agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
19918
+ containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
19919
+ };
19920
+ }
19921
+ return { entries };
19922
+ } catch (e) {
19923
+ logger.warn(`Failed to load pending marker from ${pendingPath}`, { error: String(e) });
19924
+ return { entries: {} };
19925
+ }
19926
+ }
19927
+ async function savePendingMarker(marker, pendingPath = defaultPendingPath(), logger = nullLogger) {
19928
+ if (Object.keys(marker.entries).length === 0) {
19929
+ try {
19930
+ await (0, import_promises10.unlink)(pendingPath);
19931
+ } catch (e) {
19932
+ if (!e || typeof e !== "object" || e.code !== "ENOENT") {
19933
+ logger.warn(`Failed to delete empty pending marker at ${pendingPath}`, {
19934
+ error: String(e)
19935
+ });
19936
+ }
19937
+ }
19938
+ return;
19939
+ }
19940
+ try {
19941
+ const data = {
19942
+ schema_version: SCHEMA_VERSION2,
19943
+ entries: Object.fromEntries(Object.entries(marker.entries).map(([skillId, entry]) => [
19944
+ skillId,
19945
+ {
19946
+ folder: entry.folder,
19947
+ submitted_at: entry.submittedAt,
19948
+ agent_runtime: entry.agentRuntime,
19949
+ container_key: entry.containerKey
19950
+ }
19951
+ ]))
19952
+ };
19953
+ await atomicWriteJson(pendingPath, data);
19954
+ } catch (e) {
19955
+ logger.warn(`Failed to save pending marker to ${pendingPath}`, { error: String(e) });
19956
+ }
19957
+ }
19958
+ function isPending(marker, skillId) {
19959
+ return skillId in marker.entries;
19960
+ }
19961
+ function addPending(marker, skillId, folder, origin = {}) {
19962
+ marker.entries[skillId] = {
19963
+ folder,
19964
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
19965
+ agentRuntime: origin.agentRuntime,
19966
+ containerKey: origin.containerKey
19967
+ };
19968
+ }
19969
+ function removePending(marker, skillId) {
19970
+ delete marker.entries[skillId];
19971
+ }
19972
+ function listPending(marker) {
19973
+ return Object.entries(marker.entries).map(([skillId, entry]) => ({
19974
+ skillId,
19975
+ folder: entry.folder,
19976
+ agentRuntime: entry.agentRuntime,
19977
+ containerKey: entry.containerKey
19978
+ }));
19979
+ }
19980
+
19981
+ // ../core/dist/skill-verdict-cache.js
19982
+ var import_node_path22 = require("node:path");
19983
+ init_config();
19984
+ init_file_utils();
19985
+ init_types2();
19986
+ var SCHEMA_VERSION3 = 1;
19987
+ var DEFAULT_VERDICT_TTL_MS = MS_PER_DAY;
19988
+ function parseSources(raw) {
19989
+ if (!Array.isArray(raw))
19990
+ return void 0;
19991
+ const sources = [];
19992
+ for (const item of raw) {
19993
+ if (!item || typeof item !== "object")
19994
+ continue;
19995
+ const entry = item;
19996
+ const source = {
19997
+ agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
19998
+ containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
19999
+ };
20000
+ if (source.agentRuntime || source.containerKey)
20001
+ sources.push(source);
20002
+ }
20003
+ return sources.length > 0 ? sources : void 0;
20004
+ }
20005
+ function mergeSources(existing, incoming) {
20006
+ const merged = [];
20007
+ const seen = /* @__PURE__ */ new Set();
20008
+ for (const source of [...existing ?? [], ...incoming ?? []]) {
20009
+ const key = source.agentRuntime ?? "";
20010
+ if (seen.has(key))
20011
+ continue;
20012
+ seen.add(key);
20013
+ merged.push(source);
20014
+ }
20015
+ return merged.length > 0 ? merged : void 0;
20016
+ }
20017
+ function defaultCachePath2() {
20018
+ return (0, import_node_path22.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
20019
+ }
20020
+ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger = nullLogger) {
20021
+ let raw;
20022
+ try {
20023
+ raw = await getFileContent(cachePath);
20024
+ } catch {
20025
+ return { entries: {} };
20026
+ }
20027
+ try {
20028
+ const data = JSON.parse(raw);
20029
+ const rawEntries = data.entries ?? {};
20030
+ const now = Date.now();
20031
+ const entries = {};
20032
+ let pruned = 0;
20033
+ for (const [skillId, entry] of Object.entries(rawEntries)) {
20034
+ const analyzedAt = entry.analyzed_at;
20035
+ if (!analyzedAt)
20036
+ continue;
20037
+ const ts = Date.parse(analyzedAt);
20038
+ if (!isFreshTimestamp(ts, ttlMs, now)) {
20039
+ pruned++;
20040
+ continue;
20041
+ }
20042
+ entries[skillId] = {
20043
+ verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
20044
+ summary: typeof entry.summary === "string" ? entry.summary : void 0,
20045
+ skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
20046
+ sources: parseSources(entry.sources),
20047
+ analyzedAt
20048
+ };
20049
+ }
20050
+ const cache2 = { entries };
20051
+ if (pruned > 0) {
20052
+ logger.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
20053
+ await saveSkillVerdictCache(cache2, cachePath, logger);
20054
+ }
20055
+ return cache2;
20056
+ } catch (e) {
20057
+ logger.warn(`Failed to load verdict cache from ${cachePath}`, { error: String(e) });
20058
+ return { entries: {} };
20059
+ }
20060
+ }
20061
+ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger = nullLogger) {
20062
+ try {
20063
+ const data = {
20064
+ schema_version: SCHEMA_VERSION3,
20065
+ entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
20066
+ skillId,
20067
+ {
20068
+ verdict: v.verdict,
20069
+ summary: v.summary,
20070
+ skill_name: v.skillName,
20071
+ sources: v.sources?.map((s) => ({
20072
+ agent_runtime: s.agentRuntime,
20073
+ container_key: s.containerKey
20074
+ })),
20075
+ analyzed_at: v.analyzedAt
20076
+ }
20077
+ ]))
20078
+ };
20079
+ await atomicWriteJson(cachePath, data);
20080
+ } catch (e) {
20081
+ logger.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
20082
+ }
20083
+ }
20084
+ function getVerdict(cache2, skillId, ttlMs = DEFAULT_VERDICT_TTL_MS) {
20085
+ const entry = cache2.entries[skillId];
20086
+ if (!entry)
20087
+ return null;
20088
+ const ts = Date.parse(entry.analyzedAt);
20089
+ if (!isFreshTimestamp(ts, ttlMs))
20090
+ return null;
20091
+ return entry;
20092
+ }
20093
+ function putVerdict(cache2, skillId, verdict) {
20094
+ const existing = cache2.entries[skillId];
20095
+ cache2.entries[skillId] = {
20096
+ ...verdict,
20097
+ sources: mergeSources(existing?.sources, verdict.sources),
20098
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
20099
+ };
20100
+ }
20101
+
19253
20102
  // ../core/dist/plugin-scanner.js
19254
20103
  init_types2();
19255
20104
  var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
@@ -19274,15 +20123,34 @@ var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
19274
20123
  var MAX_FILE_SIZE = 512 * 1024;
19275
20124
  async function walkPluginFiles(installPath, logger) {
19276
20125
  const files = [];
20126
+ const visited = /* @__PURE__ */ new Set();
20127
+ let rootReal;
20128
+ try {
20129
+ rootReal = await (0, import_promises11.realpath)((0, import_node_path23.resolve)(installPath));
20130
+ } catch (e) {
20131
+ logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
20132
+ return files;
20133
+ }
19277
20134
  async function walk(dirOrFile) {
20135
+ let real;
20136
+ try {
20137
+ real = await (0, import_promises11.realpath)(dirOrFile);
20138
+ } catch {
20139
+ return;
20140
+ }
20141
+ if (visited.has(real))
20142
+ return;
20143
+ if (!isContained(real, rootReal))
20144
+ return;
20145
+ visited.add(real);
19278
20146
  let stats;
19279
20147
  try {
19280
- stats = await (0, import_promises9.stat)(dirOrFile);
20148
+ stats = await (0, import_promises11.stat)(dirOrFile);
19281
20149
  } catch {
19282
20150
  return;
19283
20151
  }
19284
20152
  if (stats.isFile()) {
19285
- if (SCANNABLE_EXTENSIONS2.has((0, import_node_path18.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
20153
+ if (SCANNABLE_EXTENSIONS2.has((0, import_node_path23.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
19286
20154
  files.push(dirOrFile);
19287
20155
  }
19288
20156
  return;
@@ -19290,32 +20158,41 @@ async function walkPluginFiles(installPath, logger) {
19290
20158
  if (stats.isDirectory()) {
19291
20159
  let entries;
19292
20160
  try {
19293
- entries = await (0, import_promises9.readdir)(dirOrFile);
20161
+ entries = await (0, import_promises11.readdir)(dirOrFile);
19294
20162
  } catch {
19295
20163
  return;
19296
20164
  }
19297
20165
  for (const entry of entries) {
19298
20166
  if (SKIP_DIRS.has(entry))
19299
20167
  continue;
19300
- const fullPath = (0, import_node_path18.join)(dirOrFile, entry);
20168
+ const fullPath = (0, import_node_path23.join)(dirOrFile, entry);
19301
20169
  await walk(fullPath);
19302
20170
  }
19303
20171
  }
19304
20172
  }
19305
20173
  try {
19306
- await walk(installPath);
20174
+ await walk((0, import_node_path23.resolve)(installPath));
19307
20175
  } catch (e) {
19308
20176
  logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
19309
20177
  }
19310
20178
  return files;
19311
20179
  }
19312
20180
  async function scanPlugin(plugin, options = {}) {
19313
- const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger } = options;
20181
+ const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger, skillVerdictCachePath, skillPendingPath, loggingConfig, skillVerdictTtlMs, uploadEnabled = true, deferUnknownSkills = false, agentRuntime } = options;
19314
20182
  const result = { plugin, findings: [] };
19315
- const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger) : Promise.resolve();
20183
+ const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger, {
20184
+ verdictCachePath: skillVerdictCachePath,
20185
+ pendingPath: skillPendingPath,
20186
+ loggingConfig,
20187
+ verdictTtlMs: skillVerdictTtlMs,
20188
+ uploadEnabled,
20189
+ deferUnknownSkills,
20190
+ agentRuntime
20191
+ }) : Promise.resolve(false);
19316
20192
  const files = await walkPluginFiles(plugin.installPath, logger);
19317
20193
  if (files.length === 0) {
19318
- await skillCheckPromise;
20194
+ if (await skillCheckPromise)
20195
+ result.deferCache = true;
19319
20196
  return result;
19320
20197
  }
19321
20198
  const allUrls = [];
@@ -19331,7 +20208,7 @@ async function scanPlugin(plugin, options = {}) {
19331
20208
  }
19332
20209
  if (amsiClient) {
19333
20210
  try {
19334
- const scanName = `${plugin.key}/${(0, import_node_path18.relative)(plugin.installPath, filePath)}`;
20211
+ const scanName = `${plugin.key}/${(0, import_node_path23.relative)(plugin.installPath, filePath)}`;
19335
20212
  const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
19336
20213
  if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
19337
20214
  result.findings.push({
@@ -19339,7 +20216,7 @@ async function scanPlugin(plugin, options = {}) {
19339
20216
  title: `AMSI detection (result=${amsiResult.amsiResult})`,
19340
20217
  severity: "critical",
19341
20218
  artifact: content.slice(0, 200),
19342
- sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
20219
+ sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
19343
20220
  });
19344
20221
  }
19345
20222
  } catch {
@@ -19392,7 +20269,7 @@ async function scanPlugin(plugin, options = {}) {
19392
20269
  title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
19393
20270
  severity: "critical",
19394
20271
  artifact: fr.sha256,
19395
- sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
20272
+ sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
19396
20273
  });
19397
20274
  }
19398
20275
  }
@@ -19400,46 +20277,109 @@ async function scanPlugin(plugin, options = {}) {
19400
20277
  } catch {
19401
20278
  }
19402
20279
  })() : Promise.resolve();
19403
- await Promise.all([urlCheckPromise, fileCheckPromise, skillCheckPromise]);
20280
+ const [, , skillDeferCache] = await Promise.all([
20281
+ urlCheckPromise,
20282
+ fileCheckPromise,
20283
+ skillCheckPromise
20284
+ ]);
20285
+ if (skillDeferCache)
20286
+ result.deferCache = true;
19404
20287
  return result;
19405
20288
  }
19406
- async function runSkillCheck(plugin, findings, logger) {
20289
+ function pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict) {
20290
+ const risk = (verdict.verdict ?? "").toUpperCase();
20291
+ if (risk !== "HIGH" && risk !== "CRITICAL")
20292
+ return;
20293
+ const severity = risk === "CRITICAL" ? "critical" : "warning";
20294
+ findings.push({
20295
+ threatId: "SKILL_CHECK",
20296
+ title: verdict.summary?.trim() || (risk === "CRITICAL" ? "Malicious skill detected" : "Suspicious skill detected"),
20297
+ severity,
20298
+ artifact: skillId.slice(0, 16),
20299
+ sourceFile: (0, import_node_path23.relative)(plugin.installPath, folder) || "."
20300
+ });
20301
+ }
20302
+ async function runSkillCheck(plugin, findings, logger, paths = {}) {
19407
20303
  try {
19408
- const skills = await computeSkillIdsForRoot(plugin.installPath);
20304
+ const skills = await computeSkillIdsForRoot(plugin.installPath, logger);
19409
20305
  if (skills.length === 0)
19410
- return;
19411
- const ids = skills.map((s) => s.skillId);
20306
+ return false;
20307
+ const verdictCache = await loadSkillVerdictCache(paths.verdictCachePath, paths.verdictTtlMs, logger);
20308
+ const uncached = [];
20309
+ for (const skill of skills) {
20310
+ const cached = getVerdict(verdictCache, skill.skillId, paths.verdictTtlMs);
20311
+ if (cached) {
20312
+ pushSkillFindingIfRisky(findings, plugin, skill.skillId, skill.folder, cached);
20313
+ } else {
20314
+ uncached.push(skill);
20315
+ }
20316
+ }
20317
+ if (uncached.length === 0)
20318
+ return false;
19412
20319
  const client = new SkillCheckClient(void 0, logger);
19413
- const verdicts = await client.checkSkills(ids);
19414
- for (const { folder, skillId } of skills) {
19415
- const verdict = verdicts.get(skillId);
19416
- if (!verdict)
20320
+ const verdicts = await client.checkSkills(uncached.map((s) => s.skillId));
20321
+ const marker = await loadPendingMarker(paths.pendingPath, logger);
20322
+ let cacheModified = false;
20323
+ let markerModified = false;
20324
+ let lookupFailed = false;
20325
+ let graceDeferred = false;
20326
+ for (const { folder, skillId } of uncached) {
20327
+ if (!verdicts.has(skillId)) {
20328
+ lookupFailed = true;
19417
20329
  continue;
19418
- const risk = (verdict.overallRiskLevel ?? "").toUpperCase();
19419
- if (risk !== "HIGH" && risk !== "CRITICAL")
19420
- continue;
19421
- const severity = risk === "CRITICAL" ? "critical" : "warning";
19422
- findings.push({
19423
- threatId: "SKILL_CHECK",
19424
- title: verdict.summary?.trim() || `Risky skill detected (${risk})`,
19425
- severity,
19426
- artifact: skillId.slice(0, 16),
19427
- sourceFile: (0, import_node_path18.relative)(plugin.installPath, folder) || ".",
19428
- recommendations: verdict.recommendations.length > 0 ? verdict.recommendations : void 0
19429
- });
20330
+ }
20331
+ const verdict = verdicts.get(skillId) ?? null;
20332
+ 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;
20340
+ pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict);
20341
+ if (isPending(marker, skillId)) {
20342
+ removePending(marker, skillId);
20343
+ markerModified = true;
20344
+ }
20345
+ } else if (!isPending(marker, skillId)) {
20346
+ if (paths.uploadEnabled === false) {
20347
+ if (paths.deferUnknownSkills)
20348
+ graceDeferred = true;
20349
+ } else {
20350
+ addPending(marker, skillId, folder, {
20351
+ agentRuntime: paths.agentRuntime,
20352
+ containerKey: plugin.key
20353
+ });
20354
+ markerModified = true;
20355
+ if (paths.loggingConfig) {
20356
+ logSkillQueued(paths.loggingConfig, skillId, plugin.key, folder).catch(() => {
20357
+ });
20358
+ }
20359
+ }
20360
+ }
19430
20361
  }
20362
+ if (cacheModified)
20363
+ await saveSkillVerdictCache(verdictCache, paths.verdictCachePath, logger);
20364
+ if (markerModified)
20365
+ await savePendingMarker(marker, paths.pendingPath, logger);
20366
+ const anyStillPending = uncached.some(({ skillId }) => isPending(marker, skillId));
20367
+ return markerModified || anyStillPending || lookupFailed || graceDeferred;
19431
20368
  } catch (e) {
19432
20369
  logger.debug("Skill check failed", { error: String(e) });
20370
+ return false;
19433
20371
  }
19434
20372
  }
19435
20373
 
19436
20374
  // ../core/dist/session-start.js
19437
20375
  var import_node_child_process2 = require("node:child_process");
19438
20376
  var import_node_fs4 = require("node:fs");
20377
+ var import_node_path25 = require("node:path");
19439
20378
  init_config();
19440
20379
  init_file_utils();
19441
20380
 
19442
20381
  // ../core/dist/session-start-scan.js
20382
+ var import_node_path24 = require("node:path");
19443
20383
  init_config();
19444
20384
  init_types2();
19445
20385
  function fromCachedFinding(finding) {
@@ -19522,6 +20462,9 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19522
20462
  deniedByException: 0,
19523
20463
  findingsCount: 0
19524
20464
  };
20465
+ const skillPendingPath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_pending.json") : void 0;
20466
+ const skillVerdictCachePath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_verdict_cache.json") : void 0;
20467
+ const cacheTtlMs = skillCacheTtlMs(config2);
19525
20468
  for (const plugin of plugins) {
19526
20469
  const denyMatch = findPluginDenyException(exceptions, plugin.key);
19527
20470
  if (denyMatch) {
@@ -19546,7 +20489,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19546
20489
  stats.allowedByException += 1;
19547
20490
  continue;
19548
20491
  }
19549
- const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
20492
+ const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated, cacheTtlMs);
19550
20493
  if (cached && cached.findings.length === 0) {
19551
20494
  stats.cacheHitsClean += 1;
19552
20495
  continue;
@@ -19565,11 +20508,23 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19565
20508
  const result = await scanPlugin(plugin, {
19566
20509
  checkUrls: context.checkUrls ?? true,
19567
20510
  checkFileHashes: context.checkFileHashes ?? true,
20511
+ checkSkills: config2.skill_check.enabled,
20512
+ uploadEnabled: context.uploadUnknownSkills ?? config2.skill_check.upload_enabled,
20513
+ deferUnknownSkills: context.deferUnknownSkills,
19568
20514
  amsiClient,
19569
- logger
20515
+ logger,
20516
+ skillPendingPath,
20517
+ skillVerdictCachePath,
20518
+ skillVerdictTtlMs: cacheTtlMs,
20519
+ loggingConfig: config2.logging,
20520
+ agentRuntime: context.agentRuntime
19570
20521
  });
19571
- storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
19572
- cacheModified = true;
20522
+ if (result.deferCache) {
20523
+ logger.debug("Plugin cache deferred (skill verdict pending)", { plugin: plugin.key });
20524
+ } else {
20525
+ storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
20526
+ cacheModified = true;
20527
+ }
19573
20528
  if (result.findings.length > 0) {
19574
20529
  stats.findingsCount += result.findings.length;
19575
20530
  resultsWithFindings.push(result);
@@ -19596,6 +20551,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19596
20551
  init_types2();
19597
20552
 
19598
20553
  // ../core/dist/version-check.js
20554
+ init_config();
19599
20555
  init_types2();
19600
20556
  var DEFAULT_TIMEOUT_MS3 = 5e3;
19601
20557
  function isNewerVersion(current, latest) {
@@ -19622,11 +20578,20 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
19622
20578
  logger.debug("Skipping version check: missing installation id");
19623
20579
  return null;
19624
20580
  }
20581
+ let config2 = context.config;
20582
+ if (!config2) {
20583
+ try {
20584
+ config2 = await loadConfig(context.configPath, logger);
20585
+ } catch (err) {
20586
+ logger.debug(`Version check config load failed: ${err}`);
20587
+ }
20588
+ }
19625
20589
  const envelope = buildSageProxyEnvelope({
19626
20590
  iid: context.iid,
19627
20591
  versionApp: currentVersion,
19628
20592
  agentRuntime: context.agentRuntime,
19629
- agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown"
20593
+ agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown",
20594
+ config: config2
19630
20595
  });
19631
20596
  const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
19632
20597
  const enriched = mergeExtendedInfo(envelope, extendedInfo);
@@ -19666,6 +20631,8 @@ async function runSessionStart(ctx) {
19666
20631
  const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
19667
20632
  pruneOrphanedTmpFiles(sageDirPath).catch(() => {
19668
20633
  });
20634
+ deployConfigDefaults(sageDirPath, logger).catch(() => {
20635
+ });
19669
20636
  maybeSpawnModelDownloadWorker({
19670
20637
  sageDirPath,
19671
20638
  workerPath: ctx.modelDownloadWorkerPath,
@@ -19676,6 +20643,13 @@ async function runSessionStart(ctx) {
19676
20643
  logger
19677
20644
  }).catch(() => {
19678
20645
  });
20646
+ const rollout = await resolveSkillUploadRollout({
20647
+ sageDirPath,
20648
+ configPath: ctx.configPath,
20649
+ version: ctx.version,
20650
+ logger,
20651
+ commitNotice: ctx.commitUploadNotice ?? true
20652
+ });
19679
20653
  const iidPromise = getInstallationId(sageDirPath).catch(() => void 0);
19680
20654
  const [scanResults, versionCheck] = await Promise.all([
19681
20655
  runSessionStartScan({
@@ -19687,7 +20661,11 @@ async function runSessionStart(ctx) {
19687
20661
  configPath: ctx.configPath,
19688
20662
  scanCachePath: ctx.scanCachePath,
19689
20663
  checkUrls: ctx.checkUrls,
19690
- checkFileHashes: ctx.checkFileHashes
20664
+ checkFileHashes: ctx.checkFileHashes,
20665
+ sageDirPath,
20666
+ agentRuntime: ctx.agentRuntime,
20667
+ uploadUnknownSkills: rollout.uploadActive,
20668
+ deferUnknownSkills: rollout.deferUnknownSkills
19691
20669
  }),
19692
20670
  iidPromise.then((iid) => {
19693
20671
  if (!iid) {
@@ -19696,11 +20674,25 @@ async function runSessionStart(ctx) {
19696
20674
  return checkForUpdate(ctx.version, logger, void 0, {
19697
20675
  agentRuntime: ctx.agentRuntime,
19698
20676
  agentRuntimeVersion: ctx.agentRuntimeVersion,
19699
- iid
20677
+ iid,
20678
+ configPath: ctx.configPath
19700
20679
  });
19701
20680
  }).catch(() => null)
19702
20681
  ]);
19703
- return { scanResults, versionCheck };
20682
+ maybeSpawnSkillUploadWorker({
20683
+ sageDirPath,
20684
+ workerPath: ctx.skillUploadWorkerPath,
20685
+ configPath: ctx.configPath,
20686
+ agentRuntime: ctx.agentRuntime,
20687
+ uploadActive: rollout.uploadActive,
20688
+ logger
20689
+ }).catch(() => {
20690
+ });
20691
+ return {
20692
+ scanResults,
20693
+ versionCheck,
20694
+ notices: rollout.showNotice ? [SKILL_UPLOAD_NOTICE.id] : []
20695
+ };
19704
20696
  }
19705
20697
  async function maybeSpawnModelDownloadWorker(args) {
19706
20698
  if (!args.workerPath) {
@@ -19746,6 +20738,7 @@ async function maybeSpawnModelDownloadWorker(args) {
19746
20738
  const spawned = spawnModelDownloadWorker({
19747
20739
  sageDirPath: args.sageDirPath,
19748
20740
  workerPath: args.workerPath,
20741
+ configPath: args.configPath,
19749
20742
  agentRuntime: args.agentRuntime,
19750
20743
  agentRuntimeVersion: args.agentRuntimeVersion,
19751
20744
  versionApp: args.versionApp,
@@ -19757,6 +20750,9 @@ async function maybeSpawnModelDownloadWorker(args) {
19757
20750
  missingModels
19758
20751
  });
19759
20752
  }
20753
+ function bunRuntimeEnv() {
20754
+ return process.versions.bun ? { BUN_BE_BUN: "1" } : {};
20755
+ }
19760
20756
  function spawnModelDownloadWorker(args) {
19761
20757
  const logger = args.logger ?? nullLogger;
19762
20758
  try {
@@ -19765,7 +20761,9 @@ function spawnModelDownloadWorker(args) {
19765
20761
  stdio: "ignore",
19766
20762
  env: {
19767
20763
  ...process.env,
20764
+ ...bunRuntimeEnv(),
19768
20765
  SAGE_DIR: args.sageDirPath,
20766
+ SAGE_CONFIG_PATH: args.configPath ?? "",
19769
20767
  SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown"),
19770
20768
  SAGE_AGENT_RUNTIME_VERSION: args.agentRuntimeVersion ?? "",
19771
20769
  SAGE_VERSION_APP: args.versionApp ?? "",
@@ -19779,9 +20777,83 @@ function spawnModelDownloadWorker(args) {
19779
20777
  return false;
19780
20778
  }
19781
20779
  }
20780
+ async function maybeSpawnSkillUploadWorker(args) {
20781
+ if (!args.uploadActive) {
20782
+ args.logger.debug("Skill upload worker skipped", {
20783
+ result: "skipped",
20784
+ skippedReason: "upload_inactive"
20785
+ });
20786
+ return;
20787
+ }
20788
+ try {
20789
+ const config2 = await loadConfig(args.configPath, args.logger);
20790
+ if (!config2.skill_check.enabled) {
20791
+ args.logger.debug("Skill upload worker skipped", {
20792
+ result: "skipped",
20793
+ skippedReason: "skill_check_disabled"
20794
+ });
20795
+ return;
20796
+ }
20797
+ } catch {
20798
+ }
20799
+ if (!args.workerPath) {
20800
+ args.logger.debug("Skill upload worker skipped", {
20801
+ result: "skipped",
20802
+ skippedReason: "missing_worker_path"
20803
+ });
20804
+ return;
20805
+ }
20806
+ if (!(0, import_node_fs4.existsSync)(args.workerPath)) {
20807
+ args.logger.debug("Skill upload worker skipped", {
20808
+ result: "skipped",
20809
+ skippedReason: "worker_script_not_found",
20810
+ workerPath: args.workerPath
20811
+ });
20812
+ return;
20813
+ }
20814
+ const marker = await loadPendingMarker((0, import_node_path25.join)(args.sageDirPath, "skill_pending.json"), args.logger);
20815
+ const pendingCount = listPending(marker).length;
20816
+ if (pendingCount === 0) {
20817
+ args.logger.debug("Skill upload worker skipped", {
20818
+ result: "skipped",
20819
+ skippedReason: "no_pending_skills"
20820
+ });
20821
+ return;
20822
+ }
20823
+ const spawned = spawnSkillUploadWorker({
20824
+ sageDirPath: args.sageDirPath,
20825
+ workerPath: args.workerPath,
20826
+ agentRuntime: args.agentRuntime,
20827
+ logger: args.logger
20828
+ });
20829
+ args.logger.debug("Skill upload worker spawn completed", {
20830
+ result: spawned ? "spawned" : "failed",
20831
+ pendingCount
20832
+ });
20833
+ }
20834
+ function spawnSkillUploadWorker(args) {
20835
+ const logger = args.logger ?? nullLogger;
20836
+ try {
20837
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [args.workerPath], {
20838
+ detached: true,
20839
+ stdio: "ignore",
20840
+ env: {
20841
+ ...process.env,
20842
+ ...bunRuntimeEnv(),
20843
+ SAGE_DIR: args.sageDirPath,
20844
+ SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown")
20845
+ }
20846
+ });
20847
+ child.unref();
20848
+ return true;
20849
+ } catch (err) {
20850
+ logger.warn("Failed to spawn skill upload worker", { error: String(err) });
20851
+ return false;
20852
+ }
20853
+ }
19782
20854
 
19783
20855
  // ../core/dist/scan-handler.js
19784
- async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion) {
20856
+ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans = true) {
19785
20857
  logger.debug(`${branding2.name} plugin scan started (${context})`, {
19786
20858
  agentRuntime,
19787
20859
  pluginsCount: plugins.length,
@@ -19796,8 +20868,20 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
19796
20868
  logger,
19797
20869
  agentRuntime,
19798
20870
  agentRuntimeVersion,
19799
- modelDownloadWorkerPath
20871
+ modelDownloadWorkerPath,
20872
+ skillUploadWorkerPath,
20873
+ commitUploadNotice
19800
20874
  });
20875
+ if (result.notices && result.notices.length > 0) {
20876
+ logger.debug(`${branding2.name} session-start produced notices`, {
20877
+ context,
20878
+ noticeIds: result.notices,
20879
+ forwarded: Boolean(onNotices)
20880
+ });
20881
+ onNotices?.(result.notices);
20882
+ } else {
20883
+ logger.debug(`${branding2.name} session-start produced no notices`, { context });
20884
+ }
19801
20885
  const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
19802
20886
  const completionData = {
19803
20887
  agentRuntime,
@@ -19811,10 +20895,13 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
19811
20895
  } else {
19812
20896
  logger.debug(`${branding2.name} plugin scan (${context}) complete`, completionData);
19813
20897
  }
20898
+ if (findingsCount === 0 && !announceCleanScans) {
20899
+ return "";
20900
+ }
19814
20901
  return formatSessionStartMessage(version, result, branding2, style);
19815
20902
  }
19816
20903
  function createScanHandler(options) {
19817
- const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
20904
+ const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, onNotices, modelDownloadWorkerPath, skillUploadWorkerPath, style = "verbose", commitUploadNotice, announceCleanScans = true } = options;
19818
20905
  return async () => {
19819
20906
  try {
19820
20907
  let plugins = await discoverPlugins2();
@@ -19822,8 +20909,10 @@ function createScanHandler(options) {
19822
20909
  if (plugins.length === 0) {
19823
20910
  logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
19824
20911
  }
19825
- const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion);
19826
- onResult?.(msg);
20912
+ const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans);
20913
+ if (msg !== "") {
20914
+ onResult?.(msg);
20915
+ }
19827
20916
  } catch (e) {
19828
20917
  logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
19829
20918
  }
@@ -19861,19 +20950,19 @@ function canonicalizeToolName(map, rawName) {
19861
20950
  init_types2();
19862
20951
 
19863
20952
  // src/bundled-dirs.ts
19864
- var import_node_path19 = require("node:path");
19865
- var packageRoot = (0, import_node_path19.resolve)(__dirname, "..");
20953
+ var import_node_path26 = require("node:path");
20954
+ var packageRoot = (0, import_node_path26.resolve)(__dirname, "..");
19866
20955
  function getBundledDataDirs() {
19867
20956
  return {
19868
- threatsDir: (0, import_node_path19.join)(packageRoot, "resources", "threats"),
19869
- trustedDomainsDir: (0, import_node_path19.join)(packageRoot, "resources", "trusted-domains")
20957
+ threatsDir: (0, import_node_path26.join)(packageRoot, "resources", "threats"),
20958
+ trustedDomainsDir: (0, import_node_path26.join)(packageRoot, "resources", "trusted-domains")
19870
20959
  };
19871
20960
  }
19872
20961
  var cachedVersion;
19873
20962
  function getSageVersion() {
19874
20963
  if (cachedVersion) return cachedVersion;
19875
20964
  try {
19876
- const pkg = JSON.parse(getFileContentSync((0, import_node_path19.join)(packageRoot, "package.json"), "utf-8"));
20965
+ const pkg = JSON.parse(getFileContentSync((0, import_node_path26.join)(packageRoot, "package.json"), "utf-8"));
19877
20966
  cachedVersion = pkg.version ?? "0.0.0";
19878
20967
  } catch {
19879
20968
  cachedVersion = "0.0.0";
@@ -19882,37 +20971,41 @@ function getSageVersion() {
19882
20971
  }
19883
20972
 
19884
20973
  // src/startup-scan.ts
19885
- var import_node_path21 = require("node:path");
20974
+ var import_node_path28 = require("node:path");
19886
20975
 
19887
20976
  // src/plugin-discovery.ts
19888
- var import_promises10 = require("node:fs/promises");
20977
+ var import_promises12 = require("node:fs/promises");
19889
20978
  var import_node_os4 = require("node:os");
19890
- var import_node_path20 = require("node:path");
19891
- var DEFAULT_EXTENSIONS_DIR = (0, import_node_path20.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
19892
- async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding) {
20979
+ var import_node_path27 = require("node:path");
20980
+ var DEFAULT_EXTENSIONS_DIR = (0, import_node_path27.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
20981
+ var SKILL_ROOTS = [
20982
+ { dir: (0, import_node_path27.join)(getHomeDir(), ".agents", "skills"), tag: "agents", scope: "personal" },
20983
+ { dir: (0, import_node_path27.join)(getHomeDir(), ".openclaw", "skills"), tag: "openclaw", scope: "personal" }
20984
+ ];
20985
+ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding, skillRoots = SKILL_ROOTS) {
19893
20986
  logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
19894
20987
  path: extensionsDir
19895
20988
  });
20989
+ const plugins = [];
19896
20990
  let entries;
19897
20991
  try {
19898
- entries = await (0, import_promises10.readdir)(extensionsDir);
20992
+ entries = await (0, import_promises12.readdir)(extensionsDir);
19899
20993
  } catch {
19900
20994
  logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
19901
- return [];
20995
+ entries = [];
19902
20996
  }
19903
- const plugins = [];
19904
20997
  for (const entry of entries) {
19905
- const extDir = (0, import_node_path20.join)(extensionsDir, entry);
20998
+ const extDir = (0, import_node_path27.join)(extensionsDir, entry);
19906
20999
  let stats;
19907
21000
  try {
19908
- stats = await (0, import_promises10.stat)(extDir);
21001
+ stats = await (0, import_promises12.stat)(extDir);
19909
21002
  if (!stats.isDirectory()) continue;
19910
21003
  } catch {
19911
21004
  continue;
19912
21005
  }
19913
21006
  let pkg;
19914
21007
  try {
19915
- const pkgPath = (0, import_node_path20.join)(extDir, "package.json");
21008
+ const pkgPath = (0, import_node_path27.join)(extDir, "package.json");
19916
21009
  const raw = await getFileContent(pkgPath);
19917
21010
  pkg = JSON.parse(raw);
19918
21011
  } catch {
@@ -19934,11 +21027,14 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
19934
21027
  });
19935
21028
  }
19936
21029
  logger.debug(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
21030
+ const skills = await discoverLooseSkillsAcrossRoots(skillRoots);
21031
+ logger.debug(`${branding2.name} skill discovery: found ${skills.length} loose skill(s)`);
21032
+ plugins.push(...skills);
19937
21033
  return plugins;
19938
21034
  }
19939
21035
 
19940
21036
  // src/startup-scan.ts
19941
- function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult) {
21037
+ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult, announceCleanScans = true) {
19942
21038
  const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
19943
21039
  const version = getSageVersion();
19944
21040
  return createScanHandler({
@@ -19953,29 +21049,58 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
19953
21049
  // No agentRuntimeVersion: the OpenClaw plugin API exposes no host version.
19954
21050
  branding: branding2,
19955
21051
  onResult,
19956
- modelDownloadWorkerPath: (0, import_node_path21.resolve)(__dirname, "model-download-worker.cjs")
21052
+ // No onNotices: OpenClaw delivers the one-time notice from
21053
+ // `before_agent_start` (a different process/turn than this scan) via
21054
+ // takePendingNotices, so the scan-time callback would never fire.
21055
+ modelDownloadWorkerPath: (0, import_node_path28.resolve)(__dirname, "model-download-worker.cjs"),
21056
+ skillUploadWorkerPath: (0, import_node_path28.resolve)(__dirname, "skill-upload-worker.cjs"),
21057
+ // Don't commit the "shown" flag here — commit happens at delivery.
21058
+ commitUploadNotice: false,
21059
+ announceCleanScans
19957
21060
  });
19958
21061
  }
19959
- function createStartupScanHandler(logger, branding2 = defaultBranding, onResult) {
19960
- return createOpenClawScanHandler(logger, "startup", branding2, onResult);
19961
- }
19962
- function createSessionScanHandler(logger, branding2 = defaultBranding, onResult) {
19963
- return createOpenClawScanHandler(logger, "session", branding2, onResult);
19964
- }
19965
- function createBeforeAgentStartHandler(getFindings, clearFindings, logger, branding2 = defaultBranding) {
19966
- return () => {
19967
- const findings = getFindings();
19968
- if (!findings) return void 0;
19969
- clearFindings();
19970
- logger.debug(`${branding2.name}: surfacing plugin scan findings via before_agent_start`);
19971
- const prependContext = [
19972
- '<security-alert source="sage-plugin-scan">',
19973
- findings,
19974
- "",
19975
- "Inform the user about these security findings.",
19976
- "</security-alert>"
19977
- ].join("\n");
19978
- return { prependContext };
21062
+ function createStartupScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
21063
+ return createOpenClawScanHandler(logger, "startup", branding2, onResult, announceCleanScans);
21064
+ }
21065
+ function createSessionScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
21066
+ return createOpenClawScanHandler(logger, "session", branding2, onResult, announceCleanScans);
21067
+ }
21068
+ function createBeforeAgentStartHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
21069
+ return (notices) => {
21070
+ const findings = getSecurityFindings();
21071
+ if (!notices && !findings) {
21072
+ logger.debug(`${branding2.name}: before_agent_start - nothing pending to surface`);
21073
+ return void 0;
21074
+ }
21075
+ if (findings) clearFindings();
21076
+ logger.debug(`${branding2.name}: surfacing context via before_agent_start`, {
21077
+ hasNotices: Boolean(notices),
21078
+ hasFindings: Boolean(findings)
21079
+ });
21080
+ const blocks = [];
21081
+ if (notices) {
21082
+ blocks.push(
21083
+ [
21084
+ '<sage-notice source="sage-session-start">',
21085
+ notices,
21086
+ "",
21087
+ "Relay this notice to the user. It is informational, not a security finding.",
21088
+ "</sage-notice>"
21089
+ ].join("\n")
21090
+ );
21091
+ }
21092
+ if (findings) {
21093
+ blocks.push(
21094
+ [
21095
+ '<security-alert source="sage-plugin-scan">',
21096
+ findings,
21097
+ "",
21098
+ "Inform the user about these security findings.",
21099
+ "</security-alert>"
21100
+ ].join("\n")
21101
+ );
21102
+ }
21103
+ return { prependContext: blocks.join("\n\n") };
19979
21104
  };
19980
21105
  }
19981
21106
 
@@ -20062,6 +21187,17 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
20062
21187
  });
20063
21188
  try {
20064
21189
  const { toolName, params } = event;
21190
+ if (captureEnabled()) {
21191
+ await captureHookInput(
21192
+ "PreToolUse",
21193
+ { toolName, params, sessionKey: ctx?.sessionKey },
21194
+ {
21195
+ toolName: canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName),
21196
+ toolInput: normalizeToolInput(toolName, params)
21197
+ },
21198
+ { filePrefix: "openclaw-" }
21199
+ );
21200
+ }
20065
21201
  const artifacts = mapToolToArtifacts(toolName, params);
20066
21202
  if (!artifacts || artifacts.length === 0) {
20067
21203
  completeHook("skipped", { skippedReason: "no_artifacts" });
@@ -20102,6 +21238,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
20102
21238
  }
20103
21239
  const maxReasons = 3;
20104
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);
20105
21242
  const intersected = artifacts.filter((a) => verdict.artifacts.includes(a.value));
20106
21243
  const flaggedArtifacts = intersected.length > 0 ? intersected : artifacts;
20107
21244
  completeHook("evaluated", {
@@ -20118,6 +21255,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
20118
21255
  title: `${branding2.name}: ${verdict.category}`,
20119
21256
  description: [
20120
21257
  `Severity: ${verdict.severity}`,
21258
+ ...rule ? [`Rule: ${rule}`] : [],
20121
21259
  `Reason: ${reasons}`,
20122
21260
  `Artifacts: ${summarizeArtifacts(artifacts)}`
20123
21261
  ].join("\n"),
@@ -20187,9 +21325,9 @@ ${notice}` : notice;
20187
21325
  const onFindings = (msg) => {
20188
21326
  pendingScanFindings = msg;
20189
21327
  };
20190
- const getPendingFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
21328
+ const getPendingSecurityFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
20191
21329
  const beforeAgentStartHandler = createBeforeAgentStartHandler(
20192
- getPendingFindings,
21330
+ getPendingSecurityFindings,
20193
21331
  () => {
20194
21332
  pendingConfigurationWarnings = null;
20195
21333
  pendingScanFindings = null;
@@ -20197,16 +21335,37 @@ ${notice}` : notice;
20197
21335
  logger,
20198
21336
  branding
20199
21337
  );
21338
+ const resolveNoticeText = async () => {
21339
+ try {
21340
+ const ids = await takePendingNotices({ logger });
21341
+ const text = ids.map((id) => formatNoticeById(id, branding)).filter(Boolean).join("\n\n");
21342
+ logger.debug(`${branding.name}: notices resolved for delivery`, {
21343
+ noticeIds: ids,
21344
+ willShow: text.length > 0
21345
+ });
21346
+ return text.length > 0 ? text : null;
21347
+ } catch (e) {
21348
+ logger.warn(`${branding.name}: notice resolution failed`, { error: String(e) });
21349
+ return null;
21350
+ }
21351
+ };
20200
21352
  api.on(
20201
21353
  "before_tool_call",
20202
21354
  createToolCallHandler(approvalStore, toolLogger, threatsDir, trustedDomainsDir, branding),
20203
21355
  { priority: 100 }
20204
21356
  );
20205
- api.on("gateway_start", createStartupScanHandler(scanLogger, branding, onFindings));
20206
- api.on("session_start", createSessionScanHandler(scanLogger, branding, onFindings));
21357
+ api.on(
21358
+ "gateway_start",
21359
+ createStartupScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
21360
+ );
21361
+ api.on(
21362
+ "session_start",
21363
+ createSessionScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
21364
+ );
20207
21365
  api.on("before_agent_start", async () => {
20208
21366
  await migrationCheckPromise;
20209
- return beforeAgentStartHandler();
21367
+ const notices = await resolveNoticeText();
21368
+ return beforeAgentStartHandler(notices);
20210
21369
  });
20211
21370
  }
20212
21371
  };