@gendigital/sage-openclaw 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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";
@@ -4328,14 +4369,24 @@ var init_types2 = __esm({
4328
4369
  AmsiCheckConfigSchema = external_exports.object({
4329
4370
  enabled: external_exports.boolean().default(true)
4330
4371
  });
4372
+ SkillCheckConfigSchema = external_exports.object({
4373
+ enabled: external_exports.boolean().default(true),
4374
+ cache_ttl_days: external_exports.number().min(0).default(1),
4375
+ /**
4376
+ * Upload unknown skills (never seen by the analyzer) for deep content
4377
+ * analysis. When false, the scan still looks skills up by content hash and
4378
+ * still flags known-risky ones — but no skill content ever leaves the
4379
+ * machine and the upload worker never runs (lookup-only mode).
4380
+ */
4381
+ upload_enabled: external_exports.boolean().default(true)
4382
+ });
4331
4383
  DEFAULT_PI_HIGH_RISK_THRESHOLD = 0.99;
4332
4384
  DEFAULT_PI_MEDIUM_RISK_THRESHOLD = 0.5;
4385
+ DEFAULT_PI_TELEMETRY_THRESHOLD = 0.95;
4333
4386
  PiCheckConfigSchema = external_exports.object({
4334
4387
  enabled: external_exports.boolean().default(false),
4335
4388
  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)
4389
+ model_path: external_exports.string().optional()
4339
4390
  });
4340
4391
  ExceptionDecisionSchema = external_exports.enum(["allow", "deny"]);
4341
4392
  ExceptionMatchSchema = external_exports.enum(["executable", "domain", "path", "plugin", "regex"]);
@@ -4357,6 +4408,7 @@ var init_types2 = __esm({
4357
4408
  file_check: FileCheckConfigSchema.default({}),
4358
4409
  package_check: PackageCheckConfigSchema.default({}),
4359
4410
  amsi_check: AmsiCheckConfigSchema.default({}),
4411
+ skill_check: SkillCheckConfigSchema.default({}),
4360
4412
  pi_check: PiCheckConfigSchema.default({}),
4361
4413
  heuristics_enabled: external_exports.boolean().default(true),
4362
4414
  cache: CacheConfigSchema.default({}),
@@ -4365,6 +4417,7 @@ var init_types2 = __esm({
4365
4417
  operational_logging: OperationalLoggingConfigSchema.default({}),
4366
4418
  sensitivity: SensitivitySchema.default("balanced"),
4367
4419
  disabled_threats: external_exports.array(external_exports.string()).default([]),
4420
+ announce_clean_scans: external_exports.boolean().default(true),
4368
4421
  brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
4369
4422
  community_iq: external_exports.boolean().default(true)
4370
4423
  });
@@ -4383,13 +4436,27 @@ var init_types2 = __esm({
4383
4436
  var config_exports = {};
4384
4437
  __export(config_exports, {
4385
4438
  HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
4439
+ MS_PER_DAY: () => MS_PER_DAY,
4386
4440
  SAGE_DIR: () => SAGE_DIR,
4387
4441
  defaultConfigPath: () => defaultConfigPath,
4388
4442
  getClaudeConfigDir: () => getClaudeConfigDir,
4443
+ isFreshTimestamp: () => isFreshTimestamp,
4389
4444
  loadConfig: () => loadConfig,
4390
4445
  loadConfigSync: () => loadConfigSync,
4391
- resolvePath: () => resolvePath
4446
+ readExplicitSkillUploadEnabled: () => readExplicitSkillUploadEnabled,
4447
+ resolvePath: () => resolvePath,
4448
+ skillCacheTtlMs: () => skillCacheTtlMs
4392
4449
  });
4450
+ function skillCacheTtlMs(config2) {
4451
+ return Math.max(1, config2.skill_check.cache_ttl_days) * MS_PER_DAY;
4452
+ }
4453
+ function isFreshTimestamp(ts, ttlMs, now = Date.now()) {
4454
+ if (Number.isNaN(ts))
4455
+ return false;
4456
+ if (ts - now > CLOCK_SKEW_TOLERANCE_MS)
4457
+ return false;
4458
+ return now - ts < ttlMs;
4459
+ }
4393
4460
  function resolvedSageDir() {
4394
4461
  return resolvePath(SAGE_DIR);
4395
4462
  }
@@ -4464,7 +4531,7 @@ function sanitizeBrandKey(data, logger) {
4464
4531
  if (typeof brandKey === "string" && brandKey.length >= 1 && brandKey.length <= 32 && BRAND_KEY_RE.test(brandKey)) {
4465
4532
  return data;
4466
4533
  }
4467
- logger.warn(`Invalid brand_key in config \u2014 ignoring`, { brand_key: brandKey });
4534
+ logger.warn(`Invalid brand_key in config - ignoring`, { brand_key: brandKey });
4468
4535
  const { brand_key: _, ...rest } = data;
4469
4536
  return rest;
4470
4537
  }
@@ -4516,6 +4583,21 @@ function parseConfig(raw, path, logger) {
4516
4583
  return defaultConfig(logger);
4517
4584
  }
4518
4585
  }
4586
+ async function readExplicitSkillUploadEnabled(configPath, logger = nullLogger) {
4587
+ const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4588
+ try {
4589
+ const data = JSON.parse(await getFileContent(path));
4590
+ const skillCheck = data.skill_check;
4591
+ if (skillCheck && typeof skillCheck === "object" && !Array.isArray(skillCheck) && "upload_enabled" in skillCheck) {
4592
+ const value = skillCheck.upload_enabled;
4593
+ if (typeof value === "boolean")
4594
+ return { present: true, value };
4595
+ logger.warn("Config skill_check.upload_enabled is not a boolean; ignoring", { value });
4596
+ }
4597
+ } catch {
4598
+ }
4599
+ return { present: false, value: false };
4600
+ }
4519
4601
  async function loadConfig(configPath, logger = nullLogger) {
4520
4602
  const path = configPath ? resolvePath(configPath) : defaultConfigPath();
4521
4603
  try {
@@ -4532,7 +4614,7 @@ function loadConfigSync(configPath, logger = nullLogger) {
4532
4614
  return defaultConfig(logger);
4533
4615
  }
4534
4616
  }
4535
- var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, BRAND_KEY_RE;
4617
+ var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, MS_PER_DAY, CLOCK_SKEW_TOLERANCE_MS, BRAND_KEY_RE;
4536
4618
  var init_config = __esm({
4537
4619
  "../core/dist/config.js"() {
4538
4620
  "use strict";
@@ -4541,6 +4623,8 @@ var init_config = __esm({
4541
4623
  init_types2();
4542
4624
  SAGE_DIR = "~/.sage";
4543
4625
  HOOK_TIMEOUT_SECONDS = 8;
4626
+ MS_PER_DAY = 24 * 60 * 60 * 1e3;
4627
+ CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1e3;
4544
4628
  BRAND_KEY_RE = /^[a-z0-9_-]+$/u;
4545
4629
  }
4546
4630
  });
@@ -6805,7 +6889,7 @@ async function ensurePiDeps(modelPath, logger = nullLogger) {
6805
6889
  }
6806
6890
  }
6807
6891
  function runNpm(cwd, args, logger) {
6808
- return new Promise((resolve9, reject) => {
6892
+ return new Promise((resolve10, reject) => {
6809
6893
  const isWindows = process.platform === "win32";
6810
6894
  const npmCmd = isWindows ? "npm.cmd" : "npm";
6811
6895
  const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
@@ -6813,7 +6897,7 @@ function runNpm(cwd, args, logger) {
6813
6897
  logger.warn(`npm install error: ${stderr || error.message}`);
6814
6898
  reject(error);
6815
6899
  } else {
6816
- resolve9();
6900
+ resolve10();
6817
6901
  }
6818
6902
  });
6819
6903
  child.unref();
@@ -14139,7 +14223,7 @@ function resolveBranding(brandKey, logger) {
14139
14223
  return defaultBranding;
14140
14224
  const entry = BRANDS[brandKey];
14141
14225
  if (!entry) {
14142
- logger?.warn(`Unknown brand_key "${brandKey}" in config \u2014 using default branding`);
14226
+ logger?.warn(`Unknown brand_key "${brandKey}" in config - using default branding`);
14143
14227
  return defaultBranding;
14144
14228
  }
14145
14229
  return { ...entry, brand_key: brandKey };
@@ -14239,88 +14323,226 @@ var ApprovalStore = class {
14239
14323
  // ../core/dist/audit-log.js
14240
14324
  var import_node_crypto3 = require("node:crypto");
14241
14325
  init_config();
14326
+
14327
+ // ../core/dist/content-snapshot.js
14328
+ var import_node_os2 = require("node:os");
14329
+ var CONTENT_FIELD_LIMITS = Object.freeze({
14330
+ command: 512,
14331
+ url: 512,
14332
+ file_path: 512,
14333
+ package_name: 256,
14334
+ package_version: 128,
14335
+ package_registry: 128
14336
+ });
14337
+ function safeTruncate(value, maxLen) {
14338
+ if (maxLen <= 0)
14339
+ return "";
14340
+ if (value.length <= maxLen)
14341
+ return value;
14342
+ const cutIndex = maxLen;
14343
+ const codeUnit = value.charCodeAt(cutIndex - 1);
14344
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
14345
+ return value.slice(0, cutIndex - 1);
14346
+ }
14347
+ return value.slice(0, cutIndex);
14348
+ }
14349
+ function scrubHomePath(value) {
14350
+ const home = (0, import_node_os2.homedir)();
14351
+ if (!home)
14352
+ return value;
14353
+ const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
14354
+ if (!normalizedHome)
14355
+ return value;
14356
+ const normalizedValue = value.replace(/\\/g, "/");
14357
+ if (normalizedValue === normalizedHome)
14358
+ return "~";
14359
+ if (normalizedValue.startsWith(`${normalizedHome}/`)) {
14360
+ return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
14361
+ }
14362
+ return value;
14363
+ }
14364
+ function asString(v) {
14365
+ return typeof v === "string" && v.length > 0 ? v : void 0;
14366
+ }
14367
+ function resolveFilePath(toolInput) {
14368
+ return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
14369
+ }
14370
+ function resolveWebFetchUrl(toolInput) {
14371
+ const single = asString(toolInput.url);
14372
+ if (single)
14373
+ return single;
14374
+ const urls = toolInput.urls;
14375
+ if (Array.isArray(urls)) {
14376
+ for (const u of urls) {
14377
+ if (typeof u === "string" && u.length > 0)
14378
+ return u;
14379
+ }
14380
+ }
14381
+ return void 0;
14382
+ }
14383
+ function extractFirstApplyPatchPath(patchText) {
14384
+ if (!patchText)
14385
+ return void 0;
14386
+ const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
14387
+ if (headerMatch?.[1]) {
14388
+ const trimmed = headerMatch[1].trim();
14389
+ if (trimmed)
14390
+ return trimmed;
14391
+ }
14392
+ const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
14393
+ if (renameMatch?.[1]) {
14394
+ const raw = renameMatch[1].trim();
14395
+ const arrow = raw.indexOf(" -> ");
14396
+ if (arrow !== -1) {
14397
+ const src = raw.slice(0, arrow).trim();
14398
+ if (src)
14399
+ return src;
14400
+ const dst = raw.slice(arrow + 4).trim();
14401
+ if (dst)
14402
+ return dst;
14403
+ } else if (raw) {
14404
+ return raw;
14405
+ }
14406
+ }
14407
+ return void 0;
14408
+ }
14409
+ function extractMcpContent(toolInput) {
14410
+ const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
14411
+ const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
14412
+ return {
14413
+ command: asString(candidate.command),
14414
+ url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
14415
+ file_path: resolveFilePath(candidate)
14416
+ };
14417
+ }
14418
+ function firstUrlArtifact(artifacts) {
14419
+ for (const a of artifacts) {
14420
+ if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
14421
+ return a.value;
14422
+ }
14423
+ }
14424
+ return void 0;
14425
+ }
14426
+ function applyFieldLimits(content) {
14427
+ for (const [key, value] of Object.entries(content)) {
14428
+ if (typeof value !== "string")
14429
+ continue;
14430
+ let sanitized = value;
14431
+ if (key === "file_path" || key === "command") {
14432
+ sanitized = scrubHomePath(sanitized);
14433
+ }
14434
+ const limit = CONTENT_FIELD_LIMITS[key];
14435
+ if (limit && sanitized.length > limit) {
14436
+ sanitized = safeTruncate(sanitized, limit);
14437
+ }
14438
+ content[key] = sanitized;
14439
+ }
14440
+ }
14441
+ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
14442
+ const content = {};
14443
+ switch (toolType) {
14444
+ case "Bash": {
14445
+ const command = asString(toolInput.command);
14446
+ if (command)
14447
+ content.command = command;
14448
+ break;
14449
+ }
14450
+ case "WebFetch": {
14451
+ const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
14452
+ if (url)
14453
+ content.url = url;
14454
+ break;
14455
+ }
14456
+ case "Write":
14457
+ case "Edit":
14458
+ case "Read":
14459
+ case "Delete": {
14460
+ const filePath = resolveFilePath(toolInput);
14461
+ if (filePath)
14462
+ content.file_path = filePath;
14463
+ break;
14464
+ }
14465
+ case "ApplyPatch": {
14466
+ const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
14467
+ const filePath = extractFirstApplyPatchPath(patchText);
14468
+ if (filePath)
14469
+ content.file_path = filePath;
14470
+ break;
14471
+ }
14472
+ case "MCP": {
14473
+ const mcp = extractMcpContent(toolInput);
14474
+ if (mcp.command)
14475
+ content.command = mcp.command;
14476
+ if (mcp.url)
14477
+ content.url = mcp.url;
14478
+ if (mcp.file_path)
14479
+ content.file_path = mcp.file_path;
14480
+ break;
14481
+ }
14482
+ }
14483
+ if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
14484
+ const firstUrl = signals.url_checks[0]?.url;
14485
+ if (firstUrl)
14486
+ content.url = firstUrl;
14487
+ }
14488
+ if (signals.package_checks && signals.package_checks.length > 0) {
14489
+ const p = signals.package_checks[0];
14490
+ if (p?.package_name)
14491
+ content.package_name = p.package_name;
14492
+ if (p?.package_version)
14493
+ content.package_version = p.package_version;
14494
+ if (p?.package_registry)
14495
+ content.package_registry = p.package_registry;
14496
+ }
14497
+ applyFieldLimits(content);
14498
+ return content;
14499
+ }
14500
+
14501
+ // ../core/dist/audit-log.js
14242
14502
  init_file_utils();
14243
14503
 
14244
14504
  // ../core/dist/jsonl-log-writer.js
14245
- var import_promises = require("node:fs/promises");
14505
+ var import_promises2 = require("node:fs/promises");
14246
14506
  var import_node_path4 = require("node:path");
14247
- var import_promises2 = require("node:timers/promises");
14248
14507
  init_config();
14508
+ init_file_utils();
14249
14509
  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
14510
  async function shouldRotate(filePath, maxBytes, maxFiles) {
14254
14511
  if (maxBytes <= 0 || maxFiles <= 0)
14255
14512
  return false;
14256
14513
  try {
14257
- const s = await (0, import_promises.stat)(filePath);
14514
+ const s = await (0, import_promises2.stat)(filePath);
14258
14515
  return s.size >= maxBytes;
14259
14516
  } catch {
14260
14517
  return false;
14261
14518
  }
14262
14519
  }
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
14520
  async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
14294
14521
  if (!await shouldRotate(filePath, maxBytes, maxFiles))
14295
14522
  return;
14296
- const releaseLock = await acquireRotateLock(filePath);
14297
- if (!releaseLock)
14298
- return;
14299
- try {
14523
+ await withFileLock(filePath, async () => {
14300
14524
  if (!await shouldRotate(filePath, maxBytes, maxFiles))
14301
14525
  return;
14302
14526
  try {
14303
- await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
14527
+ await (0, import_promises2.unlink)(`${filePath}.${maxFiles}`);
14304
14528
  } catch {
14305
14529
  }
14306
14530
  for (let i = maxFiles - 1; i >= 1; i--) {
14307
14531
  try {
14308
- await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14532
+ await (0, import_promises2.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
14309
14533
  } catch {
14310
14534
  }
14311
14535
  }
14312
14536
  try {
14313
- await (0, import_promises.rename)(filePath, `${filePath}.1`);
14537
+ await (0, import_promises2.rename)(filePath, `${filePath}.1`);
14314
14538
  } catch {
14315
14539
  }
14316
- } finally {
14317
- await releaseLock();
14318
- }
14540
+ });
14319
14541
  }
14320
14542
  async function appendJsonlEntryNow(path, config2, entry) {
14321
- await (0, import_promises.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
14543
+ await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
14322
14544
  await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
14323
- await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
14545
+ await (0, import_promises2.appendFile)(path, `${JSON.stringify(entry)}
14324
14546
  `);
14325
14547
  }
14326
14548
  async function appendJsonlEntry(config2, entry) {
@@ -14338,6 +14560,7 @@ async function appendJsonlEntry(config2, entry) {
14338
14560
  }
14339
14561
 
14340
14562
  // ../core/dist/audit-log.js
14563
+ init_types2();
14341
14564
  var MAX_SUMMARY_LEN = 200;
14342
14565
  var AUDIT_LOG_SCHEMA_VERSION = 1;
14343
14566
  function toolInputSummary(toolName, toolInput) {
@@ -14406,6 +14629,22 @@ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
14406
14629
  } catch {
14407
14630
  }
14408
14631
  }
14632
+ async function logSkillQueued(config2, skillId, pluginKey, skillFolder) {
14633
+ if (!config2.enabled)
14634
+ return;
14635
+ try {
14636
+ await appendEntry(config2, {
14637
+ type: "skill_queued",
14638
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14639
+ skill_id: skillId,
14640
+ plugin_key: pluginKey,
14641
+ // Home-scrubbed folder path: aids forensics when the same skill id shows
14642
+ // up under multiple plugins. Omitted when the caller has no path.
14643
+ ...skillFolder ? { skill_folder: scrubHomePath(skillFolder) } : {}
14644
+ });
14645
+ } catch {
14646
+ }
14647
+ }
14409
14648
 
14410
14649
  // ../core/dist/cache.js
14411
14650
  init_config();
@@ -14844,19 +15083,19 @@ var PersistentPowershellAmsiBackend = class {
14844
15083
  return this.available;
14845
15084
  }
14846
15085
  waitForLine(timeout) {
14847
- return new Promise((resolve9, reject) => {
15086
+ return new Promise((resolve10, reject) => {
14848
15087
  const timer = setTimeout(() => {
14849
15088
  this.pendingResponse = null;
14850
15089
  reject(new Error("timeout"));
14851
15090
  }, timeout);
14852
- this.pendingResponse = { resolve: resolve9, reject, timer };
15091
+ this.pendingResponse = { resolve: resolve10, reject, timer };
14853
15092
  });
14854
15093
  }
14855
15094
  async enqueueScan(operation) {
14856
15095
  const previous = this.scanQueue;
14857
15096
  let release;
14858
- this.scanQueue = new Promise((resolve9) => {
14859
- release = resolve9;
15097
+ this.scanQueue = new Promise((resolve10) => {
15098
+ release = resolve10;
14860
15099
  });
14861
15100
  await previous;
14862
15101
  try {
@@ -14904,10 +15143,10 @@ var PersistentPowershellAmsiBackend = class {
14904
15143
  const line = this.stdoutBuffer.slice(0, idx).trim();
14905
15144
  this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
14906
15145
  if (this.pendingResponse) {
14907
- const { resolve: resolve9, timer } = this.pendingResponse;
15146
+ const { resolve: resolve10, timer } = this.pendingResponse;
14908
15147
  this.pendingResponse = null;
14909
15148
  clearTimeout(timer);
14910
- resolve9(line);
15149
+ resolve10(line);
14911
15150
  }
14912
15151
  idx = this.stdoutBuffer.indexOf("\n");
14913
15152
  }
@@ -15006,7 +15245,7 @@ var WslPowershellAmsiBackend = class {
15006
15245
  if (!this.available)
15007
15246
  return null;
15008
15247
  const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
15009
- return new Promise((resolve9) => {
15248
+ return new Promise((resolve10) => {
15010
15249
  try {
15011
15250
  const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
15012
15251
  let stdout = "";
@@ -15016,7 +15255,7 @@ var WslPowershellAmsiBackend = class {
15016
15255
  ps.kill();
15017
15256
  } catch {
15018
15257
  }
15019
- resolve9(null);
15258
+ resolve10(null);
15020
15259
  }, PS_TIMEOUT);
15021
15260
  ps.stdout?.on("data", (chunk) => {
15022
15261
  stdout += chunk.toString();
@@ -15029,7 +15268,7 @@ var WslPowershellAmsiBackend = class {
15029
15268
  ps.on("error", (err) => {
15030
15269
  clearTimeout(timer);
15031
15270
  this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
15032
- resolve9(null);
15271
+ resolve10(null);
15033
15272
  });
15034
15273
  ps.on("exit", () => {
15035
15274
  clearTimeout(timer);
@@ -15042,11 +15281,11 @@ var WslPowershellAmsiBackend = class {
15042
15281
  contentName
15043
15282
  });
15044
15283
  }
15045
- resolve9(null);
15284
+ resolve10(null);
15046
15285
  return;
15047
15286
  }
15048
15287
  this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
15049
- resolve9(interpretAmsiResult(amsiResult, content, contentName));
15288
+ resolve10(interpretAmsiResult(amsiResult, content, contentName));
15050
15289
  });
15051
15290
  const req = JSON.stringify({ content: truncated, contentName });
15052
15291
  ps.stdin?.write(`${req}
@@ -15054,7 +15293,7 @@ var WslPowershellAmsiBackend = class {
15054
15293
  ps.stdin?.end();
15055
15294
  } catch (e) {
15056
15295
  this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
15057
- resolve9(null);
15296
+ resolve10(null);
15058
15297
  }
15059
15298
  });
15060
15299
  }
@@ -15200,7 +15439,7 @@ init_file_utils();
15200
15439
  var import_meta = {};
15201
15440
  function resolveVersion() {
15202
15441
  if (true)
15203
- return "0.11.0";
15442
+ return "0.12.0";
15204
15443
  try {
15205
15444
  const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15206
15445
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -15349,9 +15588,10 @@ var FileCheckClient = class {
15349
15588
  var import_node_fs = require("node:fs");
15350
15589
  var import_node_path6 = require("node:path");
15351
15590
  init_config();
15352
- var MODEL_SCHEMA_VERSION = "v1";
15591
+ var MODEL_SCHEMA_VERSION = "v2";
15353
15592
  var REQUIRED_MODELS_BY_SCHEMA = {
15354
- v1: ["pi-model"]
15593
+ v1: ["pi-model"],
15594
+ v2: ["pi-model"]
15355
15595
  };
15356
15596
  var REQUIRED_MODEL_FILES = {
15357
15597
  "pi-model": [
@@ -15385,8 +15625,11 @@ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
15385
15625
  init_types2();
15386
15626
  var STALE_LOCK_MS = 60 * 60 * 1e3;
15387
15627
 
15628
+ // ../core/dist/clients/model-manifest.js
15629
+ init_config();
15630
+
15388
15631
  // ../core/dist/sage-proxy.js
15389
- function mapSageProxyOs(platform) {
15632
+ function mapSageHostOs(platform) {
15390
15633
  switch (platform) {
15391
15634
  case "win32":
15392
15635
  return "WINDOWS";
@@ -15398,21 +15641,37 @@ function mapSageProxyOs(platform) {
15398
15641
  return platform;
15399
15642
  }
15400
15643
  }
15401
- function mapSageProxyArchitecture(arch) {
15644
+ function mapSageHostArchitecture(arch) {
15402
15645
  return arch.toUpperCase();
15403
15646
  }
15647
+ function buildSageUserConfig(config2) {
15648
+ return {
15649
+ sensitivity: config2.sensitivity,
15650
+ url_check_enabled: config2.url_check.enabled,
15651
+ file_check_enabled: config2.file_check.enabled,
15652
+ package_check_enabled: config2.package_check.enabled,
15653
+ heuristics_enabled: config2.heuristics_enabled,
15654
+ pi_check_enabled: config2.pi_check.enabled,
15655
+ community_iq_enabled: config2.community_iq,
15656
+ // Optional-chained + defaulted: real callers pass a loadConfig result (skill_check
15657
+ // always present via the schema default), but this feeds fail-open telemetry, so a
15658
+ // partial config must never crash the envelope build.
15659
+ skill_check_upload_enabled: config2.skill_check?.upload_enabled ?? true
15660
+ };
15661
+ }
15404
15662
  function buildSageProxyEnvelope(args) {
15405
15663
  return {
15406
15664
  identity: { uuid: args.iid },
15407
15665
  product: { version_app: args.versionApp },
15408
15666
  platform: {
15409
- os: args.platformOs ?? mapSageProxyOs(process.platform),
15410
- architecture: args.platformArchitecture ?? mapSageProxyArchitecture(process.arch)
15667
+ os: args.platformOs ?? mapSageHostOs(process.platform),
15668
+ architecture: args.platformArchitecture ?? mapSageHostArchitecture(process.arch)
15411
15669
  },
15412
15670
  agent: {
15413
15671
  agent_runtime: args.agentRuntime,
15414
15672
  agent_runtime_version: args.agentRuntimeVersion
15415
- }
15673
+ },
15674
+ ...args.config ? { config: buildSageUserConfig(args.config) } : {}
15416
15675
  };
15417
15676
  }
15418
15677
 
@@ -15920,6 +16179,9 @@ ${content.slice(-tailLen)}`;
15920
16179
  // ../core/dist/index.js
15921
16180
  init_pi_deps_installer();
15922
16181
 
16182
+ // ../core/dist/clients/skill-analyze.js
16183
+ init_types2();
16184
+
15923
16185
  // ../core/dist/clients/skill-check.js
15924
16186
  init_types2();
15925
16187
  var DEFAULT_TIMEOUT4 = 5;
@@ -15942,9 +16204,13 @@ var SkillCheckClient = class {
15942
16204
  /**
15943
16205
  * Check a list of skill IDs against the proxy.
15944
16206
  *
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.
16207
+ * Returns a Map keyed by skill id:
16208
+ * - `null` — proxy had no opinion (explicit null in response, or skill absent
16209
+ * from an otherwise-successful response). Callers should treat this as
16210
+ * "unknown, queue for upload".
16211
+ * - absent — the entire batch failed (HTTP error or network error). Callers
16212
+ * should fail open and skip the skill rather than enqueue it.
16213
+ * - object — a verdict from the analyzer.
15948
16214
  *
15949
16215
  * Fails-open: on any error returns an empty Map. Per-batch errors do
15950
16216
  * not poison results from successful batches.
@@ -15988,8 +16254,10 @@ var SkillCheckClient = class {
15988
16254
  const data = await response.json();
15989
16255
  const results = data.results ?? {};
15990
16256
  for (const id of skillIds) {
15991
- if (!(id in results))
16257
+ if (!(id in results)) {
16258
+ out.set(id, null);
15992
16259
  continue;
16260
+ }
15993
16261
  const raw = results[id];
15994
16262
  if (raw === null || raw === void 0) {
15995
16263
  out.set(id, null);
@@ -16004,17 +16272,10 @@ var SkillCheckClient = class {
16004
16272
  return out;
16005
16273
  }
16006
16274
  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
16275
  return {
16012
16276
  skillId,
16013
16277
  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
16278
+ summary: typeof raw.summary === "string" ? raw.summary : void 0
16018
16279
  };
16019
16280
  }
16020
16281
  };
@@ -16022,6 +16283,70 @@ var SkillCheckClient = class {
16022
16283
  // ../core/dist/index.js
16023
16284
  init_config();
16024
16285
 
16286
+ // ../core/dist/config-defaults.js
16287
+ var import_promises3 = require("node:fs/promises");
16288
+ var import_node_path10 = require("node:path");
16289
+ init_file_utils();
16290
+ init_types2();
16291
+ var CONFIG_DEFAULTS_FILENAME = "config.default.json";
16292
+ var CONFIG_DEFAULTS_SCHEMA_VERSION = 1;
16293
+ function buildConfigDefaults() {
16294
+ return {
16295
+ schema_version: CONFIG_DEFAULTS_SCHEMA_VERSION,
16296
+ ...ConfigSchema.parse({})
16297
+ };
16298
+ }
16299
+ function serializeConfigDefaults() {
16300
+ return `${JSON.stringify(buildConfigDefaults(), null, 2)}
16301
+ `;
16302
+ }
16303
+ function readSchemaVersion(raw) {
16304
+ try {
16305
+ const data = JSON.parse(raw);
16306
+ if (typeof data !== "object" || data === null || Array.isArray(data))
16307
+ return null;
16308
+ const version = data.schema_version;
16309
+ return typeof version === "number" && Number.isInteger(version) ? version : null;
16310
+ } catch {
16311
+ return null;
16312
+ }
16313
+ }
16314
+ async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
16315
+ const targetPath = (0, import_node_path10.join)(sageDirPath, CONFIG_DEFAULTS_FILENAME);
16316
+ const defaults = serializeConfigDefaults();
16317
+ try {
16318
+ await (0, import_promises3.mkdir)(sageDirPath, { recursive: true, mode: 448 });
16319
+ await (0, import_promises3.chmod)(sageDirPath, 448);
16320
+ let existing = null;
16321
+ try {
16322
+ existing = await (0, import_promises3.readFile)(targetPath, "utf-8");
16323
+ } catch {
16324
+ }
16325
+ if (existing) {
16326
+ const existingVersion = readSchemaVersion(existing);
16327
+ if (existingVersion !== null && existingVersion > CONFIG_DEFAULTS_SCHEMA_VERSION) {
16328
+ logger.warn("Deployed config defaults are newer, update your installation", {
16329
+ path: targetPath,
16330
+ existingVersion,
16331
+ supportedVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
16332
+ });
16333
+ return;
16334
+ }
16335
+ if (existing === defaults)
16336
+ return;
16337
+ }
16338
+ await withFileLock(targetPath, async () => {
16339
+ await (0, import_promises3.writeFile)(targetPath, defaults, { encoding: "utf-8", mode: 384 });
16340
+ logger.debug("Deployed config defaults", {
16341
+ path: targetPath,
16342
+ schemaVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
16343
+ });
16344
+ });
16345
+ } catch (err) {
16346
+ logger.debug("Failed to deploy config defaults", { path: targetPath, error: String(err) });
16347
+ }
16348
+ }
16349
+
16025
16350
  // ../core/dist/config-diagnostics.js
16026
16351
  init_config();
16027
16352
  init_file_utils();
@@ -16158,184 +16483,10 @@ function isScannableContent(content) {
16158
16483
  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
16484
  }
16160
16485
 
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;
16216
- }
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;
16242
- }
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
- };
16251
- }
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;
16259
- }
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
- }
16274
- }
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;
16333
- }
16334
-
16335
16486
  // ../core/dist/extended-info.js
16336
- var import_promises3 = require("node:fs/promises");
16487
+ var import_promises4 = require("node:fs/promises");
16337
16488
  var import_node_os3 = require("node:os");
16338
- var import_node_path10 = require("node:path");
16489
+ var import_node_path11 = require("node:path");
16339
16490
  init_file_utils();
16340
16491
  init_types2();
16341
16492
  var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
@@ -16401,8 +16552,8 @@ function sanitizeDocument(parsed, logger) {
16401
16552
  return out;
16402
16553
  }
16403
16554
  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);
16555
+ const sageDir = sageDirPath ?? (0, import_node_path11.join)((0, import_node_os3.homedir)(), ".sage");
16556
+ const filePath = (0, import_node_path11.join)(sageDir, EXTENDED_INFO_FILENAME);
16406
16557
  const cached = cache.get(filePath);
16407
16558
  if (cached)
16408
16559
  return cached.value;
@@ -16413,7 +16564,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
16413
16564
  async function loadExtendedInfoUncached(filePath, logger) {
16414
16565
  let size;
16415
16566
  try {
16416
- const info = await (0, import_promises3.stat)(filePath);
16567
+ const info = await (0, import_promises4.stat)(filePath);
16417
16568
  if (!info.isFile()) {
16418
16569
  logger.debug(`extended-info: not a regular file at ${filePath}`);
16419
16570
  return null;
@@ -16501,13 +16652,13 @@ function mergeExtendedInfo(envelope, extendedInfo) {
16501
16652
 
16502
16653
  // ../core/dist/installation-id.js
16503
16654
  var import_node_crypto4 = require("node:crypto");
16504
- var import_promises4 = require("node:fs/promises");
16505
- var import_node_path11 = require("node:path");
16655
+ var import_promises5 = require("node:fs/promises");
16656
+ var import_node_path12 = require("node:path");
16506
16657
  init_config();
16507
16658
  init_file_utils();
16508
16659
  async function getInstallationId(sageDirPath) {
16509
16660
  const sageDir = sageDirPath ?? resolvePath("~/.sage");
16510
- const idPath = (0, import_node_path11.join)(sageDir, "installation-id");
16661
+ const idPath = (0, import_node_path12.join)(sageDir, "installation-id");
16511
16662
  let fileExists = false;
16512
16663
  try {
16513
16664
  const existing = await getFileContent(idPath, "utf-8");
@@ -16519,8 +16670,8 @@ async function getInstallationId(sageDirPath) {
16519
16670
  }
16520
16671
  try {
16521
16672
  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" });
16673
+ await (0, import_promises5.mkdir)(sageDir, { recursive: true, mode: 448 });
16674
+ await (0, import_promises5.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16524
16675
  return id;
16525
16676
  } catch (err) {
16526
16677
  if (err.code === "EEXIST") {
@@ -16549,10 +16700,10 @@ function resolveTimeoutMs() {
16549
16700
  const ms = Math.floor(parsed * 1e3);
16550
16701
  return Math.min(ms, MAX_EFFECTIVE_TIMEOUT_MS);
16551
16702
  }
16552
- async function sendCommunityIqDetection(args) {
16703
+ async function sendCommunityIqTelemetry(args) {
16553
16704
  const logger = args.logger ?? nullLogger;
16554
16705
  if (!args.communityIqEnabled) {
16555
- logger.debug("Community IQ disabled, skipping detection telemetry");
16706
+ logger.debug("Community IQ disabled, skipping telemetry");
16556
16707
  return;
16557
16708
  }
16558
16709
  let iid;
@@ -16561,15 +16712,17 @@ async function sendCommunityIqDetection(args) {
16561
16712
  } catch {
16562
16713
  }
16563
16714
  if (!iid) {
16564
- logger.debug("Skipping detection telemetry: missing installation id");
16715
+ logger.debug("Skipping telemetry: missing installation id");
16565
16716
  return;
16566
16717
  }
16567
16718
  const envelope = buildSageProxyEnvelope({
16568
16719
  iid,
16569
16720
  versionApp: VERSION,
16570
16721
  agentRuntime: args.agentRuntime ?? "unknown",
16571
- agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown"
16722
+ agentRuntimeVersion: args.agentRuntimeVersion ?? process.env.SAGE_AGENT_RUNTIME_VERSION ?? "unknown",
16723
+ config: args.config
16572
16724
  });
16725
+ const blocking = args.blocking ?? true;
16573
16726
  const payload = {
16574
16727
  ...envelope,
16575
16728
  block_event: {
@@ -16578,8 +16731,8 @@ async function sendCommunityIqDetection(args) {
16578
16731
  // canonicalize before calling `evaluateToolCall`, so no further
16579
16732
  // mapping is needed here.
16580
16733
  tool_type: args.toolName,
16581
- verdict: "deny",
16582
- user_action: "blocked",
16734
+ verdict: blocking ? "deny" : "suspicious",
16735
+ ...blocking ? { user_action: "blocked" } : {},
16583
16736
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16584
16737
  ...args.signals && Object.keys(args.signals).length > 0 ? { signals: args.signals } : {},
16585
16738
  content: args.content ?? {}
@@ -16590,20 +16743,21 @@ async function sendCommunityIqDetection(args) {
16590
16743
  const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
16591
16744
  const enriched = mergeExtendedInfo(payload, extendedInfo);
16592
16745
  const timeoutMs = resolveTimeoutMs();
16746
+ const endpoint = blocking ? "/v2/detection" : "/v2/heuristic";
16593
16747
  try {
16594
- const response = await fetch(resolveEndpoint("/v2/detection"), {
16748
+ const response = await fetch(resolveEndpoint(endpoint), {
16595
16749
  method: "POST",
16596
16750
  headers: { "Content-Type": "application/json" },
16597
16751
  body: JSON.stringify(enriched),
16598
16752
  signal: AbortSignal.timeout(timeoutMs)
16599
16753
  });
16600
16754
  if (!response.ok) {
16601
- logger.warn("Detection telemetry send failed", {
16755
+ logger.warn("Community IQ telemetry send failed", {
16602
16756
  eventId: args.eventId,
16603
16757
  status: response.status
16604
16758
  });
16605
16759
  } else {
16606
- logger.debug("Detection telemetry sent", {
16760
+ logger.debug("Community IQ telemetry sent", {
16607
16761
  eventId: args.eventId,
16608
16762
  toolName: args.toolName,
16609
16763
  hookType: args.hookType ?? "PreToolUse"
@@ -16617,6 +16771,29 @@ async function sendCommunityIqDetection(args) {
16617
16771
  }
16618
16772
  }
16619
16773
 
16774
+ // ../core/dist/e2e-capture.js
16775
+ var import_node_path13 = require("node:path");
16776
+ var CAPTURE_MAX_BYTES = 10 * 1024 * 1024;
16777
+ var CAPTURE_MAX_FILES = 2;
16778
+ function captureEnabled() {
16779
+ return !!process.env.SAGE_E2E_CAPTURE_DIR;
16780
+ }
16781
+ async function captureHookInput(event, raw, normalized, options = {}) {
16782
+ const dir = process.env.SAGE_E2E_CAPTURE_DIR;
16783
+ if (!dir)
16784
+ return;
16785
+ try {
16786
+ const prefix = options.filePrefix ?? "";
16787
+ const base = event === "PreToolUse" ? "pre-tool-use.jsonl" : "post-tool-use.jsonl";
16788
+ await appendJsonlEntry({
16789
+ path: (0, import_node_path13.join)(dir, `${prefix}${base}`),
16790
+ max_bytes: CAPTURE_MAX_BYTES,
16791
+ max_files: CAPTURE_MAX_FILES
16792
+ }, { event, raw, normalized });
16793
+ } catch {
16794
+ }
16795
+ }
16796
+
16620
16797
  // ../core/dist/policy.js
16621
16798
  init_types2();
16622
16799
  var SENSITIVITY_POLICY = {
@@ -16840,27 +17017,27 @@ init_config();
16840
17017
 
16841
17018
  // ../core/dist/exceptions.js
16842
17019
  var import_node_crypto5 = require("node:crypto");
16843
- var import_node_path13 = require("node:path");
17020
+ var import_node_path15 = require("node:path");
16844
17021
  init_config();
16845
17022
  init_file_utils();
16846
17023
 
16847
17024
  // ../core/dist/trusted-domains.js
16848
- var import_promises5 = require("node:fs/promises");
16849
- var import_node_path12 = require("node:path");
17025
+ var import_promises6 = require("node:fs/promises");
17026
+ var import_node_path14 = require("node:path");
16850
17027
  var import_yaml = __toESM(require_dist(), 1);
16851
17028
  init_file_utils();
16852
17029
  init_types2();
16853
17030
  async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
16854
17031
  let files;
16855
17032
  try {
16856
- files = (await (0, import_promises5.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
17033
+ files = (await (0, import_promises6.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
16857
17034
  } catch {
16858
17035
  logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
16859
17036
  return [];
16860
17037
  }
16861
17038
  const domains = [];
16862
17039
  for (const filename of files) {
16863
- const filePath = (0, import_node_path12.join)(trustedDomainsDir, filename);
17040
+ const filePath = (0, import_node_path14.join)(trustedDomainsDir, filename);
16864
17041
  let content;
16865
17042
  try {
16866
17043
  content = await getFileContent(filePath);
@@ -17136,7 +17313,7 @@ function matchesDomain(pattern, url) {
17136
17313
  function normalizePatternPath(pattern) {
17137
17314
  const home = getHomeDir();
17138
17315
  const expanded = pattern.startsWith("~/") || pattern === "~" ? `${home}${pattern.slice(1)}` : pattern;
17139
- return (0, import_node_path13.normalize)(expanded);
17316
+ return (0, import_node_path15.normalize)(expanded);
17140
17317
  }
17141
17318
  function matchesPath(pattern, filePath) {
17142
17319
  const hasWildcard = pattern.includes("*");
@@ -17147,7 +17324,7 @@ function matchesPath(pattern, filePath) {
17147
17324
  const normalizedPath = normalizePatternPath(filePath);
17148
17325
  if (normalizedPath === normalizedPattern)
17149
17326
  return true;
17150
- if (normalizedPath.startsWith(normalizedPattern + import_node_path13.sep))
17327
+ if (normalizedPath.startsWith(normalizedPattern + import_node_path15.sep))
17151
17328
  return true;
17152
17329
  return false;
17153
17330
  }
@@ -17927,7 +18104,7 @@ function extractFromRequirementsTxt(content) {
17927
18104
  }
17928
18105
 
17929
18106
  // ../core/dist/statusline.js
17930
- var import_node_path14 = require("node:path");
18107
+ var import_node_path16 = require("node:path");
17931
18108
  init_config();
17932
18109
  init_file_utils();
17933
18110
  var STATUS_PREFIX = "statusline-";
@@ -17936,15 +18113,17 @@ function sanitizeSessionId(sessionId) {
17936
18113
  return sessionId.replace(/[^a-zA-Z0-9-]/g, "_");
17937
18114
  }
17938
18115
  function statusFilePath(sessionId) {
17939
- return (0, import_node_path14.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
18116
+ return (0, import_node_path16.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
17940
18117
  }
17941
18118
  function emptyStatus() {
18119
+ const now = (/* @__PURE__ */ new Date()).toISOString();
17942
18120
  return {
17943
18121
  denied: 0,
17944
18122
  flagged: 0,
17945
18123
  lastCategory: null,
17946
18124
  lastReason: null,
17947
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18125
+ updatedAt: now,
18126
+ startedAt: now
17948
18127
  };
17949
18128
  }
17950
18129
  async function readStatus(sessionId) {
@@ -17969,8 +18148,8 @@ async function updateSessionStatus(sessionId, verdict) {
17969
18148
  }
17970
18149
 
17971
18150
  // ../core/dist/threat-loader.js
17972
- var import_promises6 = require("node:fs/promises");
17973
- var import_node_path15 = require("node:path");
18151
+ var import_promises7 = require("node:fs/promises");
18152
+ var import_node_path17 = require("node:path");
17974
18153
  var import_yaml2 = __toESM(require_dist(), 1);
17975
18154
  init_file_utils();
17976
18155
  init_types2();
@@ -18003,13 +18182,13 @@ async function loadThreats(threatDir, logger = nullLogger) {
18003
18182
  const threats = [];
18004
18183
  let files;
18005
18184
  try {
18006
- files = (await (0, import_promises6.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
18185
+ files = (await (0, import_promises7.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
18007
18186
  } catch {
18008
18187
  logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
18009
18188
  return threats;
18010
18189
  }
18011
18190
  for (const filename of files) {
18012
- const filePath = (0, import_node_path15.join)(threatDir, filename);
18191
+ const filePath = (0, import_node_path17.join)(threatDir, filename);
18013
18192
  let content;
18014
18193
  try {
18015
18194
  content = await getFileContent(filePath);
@@ -18365,8 +18544,8 @@ async function evaluateToolCall(request, context) {
18365
18544
  amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
18366
18545
  piCheckResults: allPiResults,
18367
18546
  piThresholds: {
18368
- highRisk: config2.pi_check.high_risk_threshold,
18369
- mediumRisk: config2.pi_check.medium_risk_threshold
18547
+ highRisk: DEFAULT_PI_HIGH_RISK_THRESHOLD,
18548
+ mediumRisk: DEFAULT_PI_MEDIUM_RISK_THRESHOLD
18370
18549
  }
18371
18550
  });
18372
18551
  } else {
@@ -18457,7 +18636,7 @@ async function evaluateToolCall(request, context) {
18457
18636
  }
18458
18637
  }
18459
18638
  if (allPiResults.length > 0) {
18460
- const piSnippetFloor = config2.pi_check.medium_risk_threshold;
18639
+ const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
18461
18640
  auditSignals.pi_checks = allPiResults.map((r) => ({
18462
18641
  risk: r.risk,
18463
18642
  model_id: r.modelId,
@@ -18491,9 +18670,9 @@ async function evaluateToolCall(request, context) {
18491
18670
  } catch (error) {
18492
18671
  logger.debug("Audit verdict logging failed open", { error: String(error) });
18493
18672
  }
18494
- if (verdict.decision === "deny") {
18673
+ const submitTelemetry = async (blocking) => {
18495
18674
  try {
18496
- await sendCommunityIqDetection({
18675
+ await sendCommunityIqTelemetry({
18497
18676
  eventId,
18498
18677
  agentRuntime: request.agentRuntime,
18499
18678
  agentRuntimeVersion: request.agentRuntimeVersion,
@@ -18502,11 +18681,18 @@ async function evaluateToolCall(request, context) {
18502
18681
  content: resolvedContent,
18503
18682
  signals: resolvedSignals,
18504
18683
  communityIqEnabled: config2.community_iq,
18684
+ config: config2,
18685
+ blocking,
18505
18686
  logger
18506
18687
  });
18507
18688
  } catch (error) {
18508
- logger.debug("Detection telemetry failed open", { error: String(error) });
18689
+ logger.debug("Community IQ telemetry failed open", { error: String(error) });
18509
18690
  }
18691
+ };
18692
+ if (verdict.decision === "deny") {
18693
+ await submitTelemetry(true);
18694
+ } else if (allPiResults.some((r) => r.risk >= DEFAULT_PI_TELEMETRY_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD)) {
18695
+ await submitTelemetry(false);
18510
18696
  }
18511
18697
  if (verdict.decision !== "allow") {
18512
18698
  try {
@@ -18515,7 +18701,7 @@ async function evaluateToolCall(request, context) {
18515
18701
  logger.debug("Session status update failed open", { error: String(error) });
18516
18702
  }
18517
18703
  }
18518
- const piWarnings = allPiResults.filter((r) => r.risk >= config2.pi_check.medium_risk_threshold && r.risk < config2.pi_check.high_risk_threshold);
18704
+ const piWarnings = allPiResults.filter((r) => r.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD);
18519
18705
  if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
18520
18706
  verdict.piWarnings = piWarnings;
18521
18707
  logEvaluationCompleted(verdict);
@@ -18657,7 +18843,7 @@ function createPiProvider(config2, _context, logger) {
18657
18843
  return new BundledPiProvider({
18658
18844
  modelPath: config2.pi_check.model_path,
18659
18845
  maxContentLength: config2.pi_check.max_content_length,
18660
- mediumRiskThreshold: config2.pi_check.medium_risk_threshold,
18846
+ mediumRiskThreshold: DEFAULT_PI_MEDIUM_RISK_THRESHOLD,
18661
18847
  logger
18662
18848
  });
18663
18849
  }
@@ -18837,18 +19023,226 @@ function formatDenyMessage(verdict, branding2 = defaultBranding) {
18837
19023
  ].join("\n");
18838
19024
  }
18839
19025
 
18840
- // ../core/dist/model-download.js
18841
- init_types2();
19026
+ // ../core/dist/install-state.js
19027
+ var import_node_path18 = require("node:path");
19028
+ init_config();
19029
+ init_file_utils();
18842
19030
 
18843
- // ../core/dist/operational-log.js
18844
- var LEVEL_PRIORITY = {
18845
- debug: 10,
18846
- info: 20,
18847
- warn: 30,
18848
- error: 40
19031
+ // ../core/dist/notices.js
19032
+ init_config();
19033
+ var SKILL_UPLOAD_NOTICE = {
19034
+ id: "skill_upload_v1",
19035
+ body: () => [
19036
+ "unknown skills \u2014 each skill's SKILL.md and every supporting file in its",
19037
+ "folder \u2014 will be uploaded and checked against potential malicious artifacts",
19038
+ "starting next session. To keep skill content on this machine, set",
19039
+ '"skill_check": { "upload_enabled": false } in ~/.sage/config.json.'
19040
+ ],
19041
+ // Suppressed once the user has explicitly set upload_enabled either way: an
19042
+ // explicit choice needs no consent prompt (the rollout obeys it directly).
19043
+ isEligible: async ({ configPath, logger }) => !(await readExplicitSkillUploadEnabled(configPath, logger)).present
18849
19044
  };
18850
- var FLUSH_TIMEOUT_MS = 250;
18851
- function shouldLog(config2, level) {
19045
+ var NOTICES = {
19046
+ [SKILL_UPLOAD_NOTICE.id]: SKILL_UPLOAD_NOTICE
19047
+ };
19048
+ var INFO_ICON = "\u2139\uFE0F";
19049
+ var CONTINUATION_INDENT = " ";
19050
+ function formatNotice(notice, branding2 = defaultBranding) {
19051
+ const [first = "", ...rest] = notice.body(branding2);
19052
+ const head = `${INFO_ICON} ${branding2.name}: ${first}`;
19053
+ return [head, ...rest.map((line) => `${CONTINUATION_INDENT}${line}`)].join("\n");
19054
+ }
19055
+ function formatNoticeById(id, branding2 = defaultBranding) {
19056
+ const notice = NOTICES[id];
19057
+ return notice ? formatNotice(notice, branding2) : "";
19058
+ }
19059
+
19060
+ // ../core/dist/install-state.js
19061
+ init_types2();
19062
+ var INSTALL_STATE_SCHEMA_VERSION = 1;
19063
+ var SKILL_UPLOAD_NOTICE_ID = SKILL_UPLOAD_NOTICE.id;
19064
+ function defaultStatePath(sageDirPath) {
19065
+ return (0, import_node_path18.join)(sageDirPath ?? resolvePath(SAGE_DIR), "install-state.json");
19066
+ }
19067
+ function emptyState() {
19068
+ return { schemaVersion: INSTALL_STATE_SCHEMA_VERSION, notices: {} };
19069
+ }
19070
+ async function loadInstallState(sageDirPath, logger = nullLogger) {
19071
+ let raw;
19072
+ try {
19073
+ raw = await getFileContent(defaultStatePath(sageDirPath));
19074
+ } catch {
19075
+ return emptyState();
19076
+ }
19077
+ try {
19078
+ const data = JSON.parse(raw);
19079
+ const rawNotices = data.notices ?? {};
19080
+ const notices = {};
19081
+ for (const [id, rec] of Object.entries(rawNotices)) {
19082
+ notices[id] = {
19083
+ noticed: rec.noticed === true,
19084
+ version: typeof rec.version === "string" ? rec.version : void 0
19085
+ };
19086
+ }
19087
+ return {
19088
+ schemaVersion: typeof data.schema_version === "number" ? data.schema_version : INSTALL_STATE_SCHEMA_VERSION,
19089
+ lastRunVersion: typeof data.last_run_version === "string" ? data.last_run_version : void 0,
19090
+ notices
19091
+ };
19092
+ } catch (e) {
19093
+ logger.warn("Failed to parse install state; treating as fresh", { error: String(e) });
19094
+ return emptyState();
19095
+ }
19096
+ }
19097
+ async function saveInstallState(state, sageDirPath, logger = nullLogger) {
19098
+ try {
19099
+ const data = {
19100
+ schema_version: state.schemaVersion,
19101
+ last_run_version: state.lastRunVersion,
19102
+ notices: Object.fromEntries(Object.entries(state.notices).map(([id, rec]) => [
19103
+ id,
19104
+ { noticed: rec.noticed, version: rec.version }
19105
+ ]))
19106
+ };
19107
+ await atomicWriteJson(defaultStatePath(sageDirPath), data);
19108
+ } catch (e) {
19109
+ logger.warn("Failed to save install state", { error: String(e) });
19110
+ }
19111
+ }
19112
+ function decideSkillUploadRollout(state, opts = {}) {
19113
+ const versionChanged = opts.version !== void 0 && state.lastRunVersion !== opts.version;
19114
+ const withVersion = () => ({ ...state, lastRunVersion: opts.version });
19115
+ if (opts.explicitConfig) {
19116
+ return {
19117
+ uploadActive: opts.explicitValue === true,
19118
+ showNotice: false,
19119
+ nextState: versionChanged ? withVersion() : null
19120
+ };
19121
+ }
19122
+ const notice = state.notices[SKILL_UPLOAD_NOTICE_ID];
19123
+ if (notice?.noticed === true) {
19124
+ return {
19125
+ uploadActive: true,
19126
+ showNotice: false,
19127
+ nextState: versionChanged ? withVersion() : null
19128
+ };
19129
+ }
19130
+ const nextState = {
19131
+ ...state,
19132
+ lastRunVersion: opts.version ?? state.lastRunVersion,
19133
+ notices: {
19134
+ ...state.notices,
19135
+ [SKILL_UPLOAD_NOTICE_ID]: { noticed: true, version: opts.version }
19136
+ }
19137
+ };
19138
+ return { uploadActive: false, showNotice: true, nextState };
19139
+ }
19140
+ var processRollout;
19141
+ async function resolveSkillUploadRollout(args) {
19142
+ const commitNotice = args.commitNotice ?? true;
19143
+ const logger = args.logger ?? nullLogger;
19144
+ let explicit;
19145
+ try {
19146
+ explicit = await readExplicitSkillUploadEnabled(args.configPath, logger);
19147
+ } catch {
19148
+ explicit = { present: false, value: false };
19149
+ }
19150
+ if (explicit.present) {
19151
+ const resolved = {
19152
+ uploadActive: explicit.value === true,
19153
+ showNotice: false,
19154
+ deferUnknownSkills: false
19155
+ };
19156
+ logger.debug("Skill-upload rollout resolved", {
19157
+ explicitConfig: true,
19158
+ explicitValue: explicit.value,
19159
+ uploadActive: resolved.uploadActive,
19160
+ showNotice: false,
19161
+ deferUnknownSkills: false,
19162
+ commitNotice,
19163
+ statePersisted: false,
19164
+ version: args.version
19165
+ });
19166
+ return resolved;
19167
+ }
19168
+ if (processRollout)
19169
+ return processRollout;
19170
+ try {
19171
+ const state = await loadInstallState(args.sageDirPath, logger);
19172
+ const decision = decideSkillUploadRollout(state, { version: args.version });
19173
+ if (commitNotice) {
19174
+ if (decision.nextState) {
19175
+ await saveInstallState(decision.nextState, args.sageDirPath, logger);
19176
+ }
19177
+ processRollout = {
19178
+ uploadActive: decision.uploadActive,
19179
+ showNotice: decision.showNotice,
19180
+ deferUnknownSkills: decision.showNotice
19181
+ };
19182
+ } else {
19183
+ processRollout = {
19184
+ uploadActive: decision.uploadActive,
19185
+ showNotice: false,
19186
+ deferUnknownSkills: decision.showNotice
19187
+ };
19188
+ }
19189
+ logger.debug("Skill-upload rollout resolved", {
19190
+ explicitConfig: false,
19191
+ uploadActive: processRollout.uploadActive,
19192
+ showNotice: processRollout.showNotice,
19193
+ deferUnknownSkills: processRollout.deferUnknownSkills,
19194
+ commitNotice,
19195
+ statePersisted: commitNotice && decision.nextState !== null,
19196
+ version: args.version
19197
+ });
19198
+ } catch (e) {
19199
+ logger.warn("Skill-upload rollout resolution failed; suppressing upload", {
19200
+ error: String(e)
19201
+ });
19202
+ processRollout = { uploadActive: false, showNotice: commitNotice, deferUnknownSkills: true };
19203
+ }
19204
+ return processRollout;
19205
+ }
19206
+ async function takePendingNotices(args) {
19207
+ const logger = args.logger ?? nullLogger;
19208
+ try {
19209
+ const state = await loadInstallState(args.sageDirPath, logger);
19210
+ const toShow = [];
19211
+ for (const notice of Object.values(NOTICES)) {
19212
+ if (state.notices[notice.id]?.noticed === true)
19213
+ continue;
19214
+ const eligible = notice.isEligible ? await notice.isEligible({ configPath: args.configPath, logger }) : true;
19215
+ if (eligible)
19216
+ toShow.push(notice.id);
19217
+ }
19218
+ if (toShow.length === 0)
19219
+ return [];
19220
+ const nextNotices = { ...state.notices };
19221
+ for (const id of toShow)
19222
+ nextNotices[id] = { noticed: true };
19223
+ await saveInstallState({ ...state, notices: nextNotices }, args.sageDirPath, logger);
19224
+ logger.debug("Pending notices taken for delivery", { noticeIds: toShow });
19225
+ return toShow;
19226
+ } catch (e) {
19227
+ logger.warn("takePendingNotices failed; showing no notices this turn", {
19228
+ error: String(e)
19229
+ });
19230
+ return [];
19231
+ }
19232
+ }
19233
+
19234
+ // ../core/dist/model-download.js
19235
+ init_types2();
19236
+
19237
+ // ../core/dist/operational-log.js
19238
+ var LEVEL_PRIORITY = {
19239
+ debug: 10,
19240
+ info: 20,
19241
+ warn: 30,
19242
+ error: 40
19243
+ };
19244
+ var FLUSH_TIMEOUT_MS = 250;
19245
+ function shouldLog(config2, level) {
18852
19246
  return config2.enabled && LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[config2.level];
18853
19247
  }
18854
19248
  function normalizeData(data) {
@@ -18908,8 +19302,8 @@ function createOperationalLogger(config2, runtime) {
18908
19302
  try {
18909
19303
  const result = await Promise.race([
18910
19304
  Promise.allSettled([...pendingWrites]).then(() => "drained"),
18911
- new Promise((resolve9) => {
18912
- timeout = setTimeout(() => resolve9("timeout"), remainingMs);
19305
+ new Promise((resolve10) => {
19306
+ timeout = setTimeout(() => resolve10("timeout"), remainingMs);
18913
19307
  timeout.unref?.();
18914
19308
  })
18915
19309
  ]);
@@ -18958,13 +19352,14 @@ function createOperationalLogger(config2, runtime) {
18958
19352
 
18959
19353
  // ../core/dist/plugin-scan-cache.js
18960
19354
  var import_node_crypto7 = require("node:crypto");
18961
- var import_promises7 = require("node:fs/promises");
18962
- var import_node_path16 = require("node:path");
19355
+ var import_promises8 = require("node:fs/promises");
19356
+ var import_node_path19 = require("node:path");
19357
+ init_config();
18963
19358
  init_file_utils();
18964
19359
  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;
19360
+ var DEFAULT_CACHE_PATH = (0, import_node_path19.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
19361
+ var DEFAULT_CACHE_TTL_MS = MS_PER_DAY;
19362
+ var SCHEMA_VERSION = 5;
18968
19363
  function cacheKey(pluginKey, version, lastUpdated) {
18969
19364
  return `${pluginKey}:${version}:${lastUpdated}`;
18970
19365
  }
@@ -18975,13 +19370,13 @@ async function computeConfigHash(sageVersion, ...dirs) {
18975
19370
  for (const dir of dirs) {
18976
19371
  let files;
18977
19372
  try {
18978
- files = (await (0, import_promises7.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
19373
+ files = (await (0, import_promises8.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
18979
19374
  } catch {
18980
19375
  continue;
18981
19376
  }
18982
19377
  for (const file of files) {
18983
19378
  try {
18984
- const content = await getFileContent((0, import_node_path16.join)(dir, file));
19379
+ const content = await getFileContent((0, import_node_path19.join)(dir, file));
18985
19380
  h.update(content);
18986
19381
  } catch {
18987
19382
  }
@@ -19050,7 +19445,7 @@ async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nu
19050
19445
  logger.warn(`Failed to save scan cache to ${cachePath}`, { error: String(e) });
19051
19446
  }
19052
19447
  }
19053
- function isCached(cache2, pluginKey, version, lastUpdated) {
19448
+ function isCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
19054
19449
  const key = cacheKey(pluginKey, version, lastUpdated);
19055
19450
  const entry = cache2.entries[key];
19056
19451
  if (!entry)
@@ -19058,13 +19453,13 @@ function isCached(cache2, pluginKey, version, lastUpdated) {
19058
19453
  try {
19059
19454
  const scannedAt = new Date(entry.scannedAt);
19060
19455
  const age = Date.now() - scannedAt.getTime();
19061
- return age < CACHE_TTL_DAYS * 24 * 60 * 60 * 1e3;
19456
+ return age < ttlMs;
19062
19457
  } catch {
19063
19458
  return false;
19064
19459
  }
19065
19460
  }
19066
- function getCached(cache2, pluginKey, version, lastUpdated) {
19067
- if (!isCached(cache2, pluginKey, version, lastUpdated))
19461
+ function getCached(cache2, pluginKey, version, lastUpdated, ttlMs = DEFAULT_CACHE_TTL_MS) {
19462
+ if (!isCached(cache2, pluginKey, version, lastUpdated, ttlMs))
19068
19463
  return null;
19069
19464
  const key = cacheKey(pluginKey, version, lastUpdated);
19070
19465
  return cache2.entries[key] ?? null;
@@ -19081,18 +19476,21 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
19081
19476
 
19082
19477
  // ../core/dist/plugin-scanner.js
19083
19478
  var import_node_crypto9 = require("node:crypto");
19084
- var import_promises9 = require("node:fs/promises");
19085
- var import_node_path18 = require("node:path");
19479
+ var import_promises11 = require("node:fs/promises");
19480
+ var import_node_path23 = require("node:path");
19086
19481
  init_config();
19087
19482
  init_file_utils();
19088
19483
 
19089
19484
  // ../core/dist/skill-id.js
19090
19485
  var import_node_crypto8 = require("node:crypto");
19091
- var import_promises8 = require("node:fs/promises");
19092
- var import_node_path17 = require("node:path");
19486
+ var import_promises9 = require("node:fs/promises");
19487
+ var import_node_path20 = require("node:path");
19093
19488
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
19489
+ var MAX_SKILL_BYTES = 50 * 1024 * 1024;
19490
+ var SkillTooLargeError = class extends Error {
19491
+ };
19094
19492
  function isContained(childReal, rootReal) {
19095
- return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path17.sep}`);
19493
+ return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path20.sep}`);
19096
19494
  }
19097
19495
  function normalizePath(p) {
19098
19496
  let out = p.replace(/\\/g, "/").normalize("NFC");
@@ -19102,47 +19500,51 @@ function normalizePath(p) {
19102
19500
  }
19103
19501
  return out;
19104
19502
  }
19105
- async function entriesFromDirectory(dirPath) {
19106
- const absDir = (0, import_node_path17.resolve)(dirPath);
19107
- const rootReal = await (0, import_promises8.realpath)(absDir);
19503
+ async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY) {
19504
+ const absDir = (0, import_node_path20.resolve)(dirPath);
19505
+ const rootReal = await (0, import_promises9.realpath)(absDir);
19108
19506
  const entries = [];
19109
19507
  const visited = /* @__PURE__ */ new Set();
19508
+ let total = 0;
19110
19509
  async function walk(currentPath) {
19111
- const real = await (0, import_promises8.realpath)(currentPath);
19510
+ const real = await (0, import_promises9.realpath)(currentPath);
19112
19511
  if (visited.has(real))
19113
19512
  return;
19114
19513
  if (!isContained(real, rootReal))
19115
19514
  return;
19116
19515
  visited.add(real);
19117
- const items = await (0, import_promises8.readdir)(currentPath);
19516
+ const items = await (0, import_promises9.readdir)(currentPath);
19118
19517
  const dirs = [];
19119
19518
  const files = [];
19120
19519
  for (const item of items) {
19121
- const fullPath = (0, import_node_path17.join)(currentPath, item);
19122
- const st = await (0, import_promises8.stat)(fullPath);
19520
+ const fullPath = (0, import_node_path20.join)(currentPath, item);
19521
+ const st = await (0, import_promises9.stat)(fullPath);
19123
19522
  if (st.isDirectory())
19124
19523
  dirs.push(item);
19125
19524
  else if (st.isFile())
19126
- files.push(item);
19525
+ files.push({ name: item, size: st.size });
19127
19526
  }
19128
19527
  dirs.sort();
19129
- files.sort();
19528
+ files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
19130
19529
  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, "/");
19530
+ const fullPath = (0, import_node_path20.join)(currentPath, d);
19531
+ const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
19133
19532
  entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
19134
19533
  await walk(fullPath);
19135
19534
  }
19136
19535
  for (const f of files) {
19137
- const fullPath = (0, import_node_path17.join)(currentPath, f);
19138
- const fileReal = await (0, import_promises8.realpath)(fullPath);
19536
+ const fullPath = (0, import_node_path20.join)(currentPath, f.name);
19537
+ const fileReal = await (0, import_promises9.realpath)(fullPath);
19139
19538
  if (!isContained(fileReal, rootReal))
19140
19539
  continue;
19141
- const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
19540
+ total += f.size;
19541
+ if (total > maxBytes)
19542
+ throw new SkillTooLargeError();
19543
+ const relPath = (0, import_node_path20.relative)(absDir, fullPath).replace(/\\/g, "/");
19142
19544
  entries.push({
19143
19545
  entryPath: relPath,
19144
19546
  isDir: false,
19145
- content: await (0, import_promises8.readFile)(fullPath)
19547
+ content: await (0, import_promises9.readFile)(fullPath)
19146
19548
  });
19147
19549
  }
19148
19550
  }
@@ -19179,77 +19581,343 @@ function computeSkillId(entries) {
19179
19581
  }
19180
19582
  return { skillId: skillHasher.digest("hex"), fileHashes };
19181
19583
  }
19182
- async function findSkillPackages(rootDir) {
19584
+ async function findSkillPackagesWithMtime(rootDir) {
19183
19585
  const found = [];
19184
19586
  let rootReal;
19185
19587
  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));
19588
+ rootReal = await (0, import_promises9.realpath)((0, import_node_path20.resolve)(rootDir));
19190
19589
  } catch {
19191
19590
  return found;
19192
19591
  }
19193
19592
  const visited = /* @__PURE__ */ new Set();
19194
- async function walk(dir) {
19593
+ async function walk(currentPath) {
19195
19594
  let real;
19196
19595
  try {
19197
- real = await (0, import_promises8.realpath)(dir);
19596
+ real = await (0, import_promises9.realpath)(currentPath);
19198
19597
  } catch {
19199
- return;
19598
+ return 0;
19200
19599
  }
19201
19600
  if (visited.has(real))
19202
- return;
19601
+ return 0;
19203
19602
  if (!isContained(real, rootReal))
19204
- return;
19603
+ return 0;
19205
19604
  visited.add(real);
19605
+ let st;
19606
+ try {
19607
+ st = await (0, import_promises9.stat)(currentPath);
19608
+ } catch {
19609
+ return 0;
19610
+ }
19611
+ let subtreeMax = st.mtimeMs;
19612
+ if (!st.isDirectory())
19613
+ return subtreeMax;
19206
19614
  let items;
19207
19615
  try {
19208
- items = await (0, import_promises8.readdir)(dir);
19616
+ items = await (0, import_promises9.readdir)(currentPath);
19209
19617
  } catch {
19210
- return;
19618
+ return subtreeMax;
19211
19619
  }
19212
19620
  items.sort();
19621
+ let entry;
19213
19622
  if (items.includes("SKILL.md")) {
19214
19623
  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);
19624
+ if ((await (0, import_promises9.stat)((0, import_node_path20.join)(currentPath, "SKILL.md"))).isFile()) {
19625
+ entry = { folder: currentPath, newestMtimeMs: 0 };
19626
+ found.push(entry);
19627
+ }
19218
19628
  } catch {
19219
19629
  }
19220
19630
  }
19221
19631
  for (const item of items) {
19222
19632
  if (SKIP_DIRS.has(item))
19223
19633
  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
- }
19634
+ const childMax = await walk((0, import_node_path20.join)(currentPath, item));
19635
+ if (childMax > subtreeMax)
19636
+ subtreeMax = childMax;
19234
19637
  }
19638
+ if (entry)
19639
+ entry.newestMtimeMs = subtreeMax;
19640
+ return subtreeMax;
19235
19641
  }
19236
- await walk((0, import_node_path17.resolve)(rootDir));
19642
+ await walk((0, import_node_path20.resolve)(rootDir));
19237
19643
  return found;
19238
19644
  }
19239
- async function computeSkillIdsForRoot(rootDir) {
19240
- const folders = await findSkillPackages(rootDir);
19645
+ function looseSkillKey(skillsRoot, folder, tag, scope) {
19646
+ 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);
19647
+ return `skill:${tag}/${rel}@${scope}`;
19648
+ }
19649
+ async function discoverLooseSkills(skillsDir, tag, scope) {
19650
+ const resolvedRoot = (0, import_node_path20.resolve)(skillsDir);
19651
+ const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0, import_node_path20.resolve)(p.folder) !== resolvedRoot);
19652
+ return packages.map(({ folder, newestMtimeMs }) => ({
19653
+ key: looseSkillKey(resolvedRoot, folder, tag, scope),
19654
+ installPath: folder,
19655
+ version: scope,
19656
+ lastUpdated: new Date(newestMtimeMs).toISOString()
19657
+ }));
19658
+ }
19659
+ async function dedupeSkillsByResolvedPath(skills) {
19660
+ const seen = /* @__PURE__ */ new Set();
19661
+ const out = [];
19662
+ for (const skill of skills) {
19663
+ let canonical = skill.installPath;
19664
+ try {
19665
+ canonical = await (0, import_promises9.realpath)(skill.installPath);
19666
+ } catch {
19667
+ }
19668
+ if (seen.has(canonical))
19669
+ continue;
19670
+ seen.add(canonical);
19671
+ out.push(skill);
19672
+ }
19673
+ return out;
19674
+ }
19675
+ async function discoverLooseSkillsAcrossRoots(roots) {
19676
+ const lists = await Promise.all(roots.map(async ({ dir, tag, scope }) => {
19677
+ try {
19678
+ return await discoverLooseSkills(dir, tag, scope);
19679
+ } catch {
19680
+ return [];
19681
+ }
19682
+ }));
19683
+ return lists.length <= 1 ? lists[0] ?? [] : dedupeSkillsByResolvedPath(lists.flat());
19684
+ }
19685
+ async function computeSkillIdsForRoot(rootDir, logger) {
19686
+ const folders = (await findSkillPackagesWithMtime(rootDir)).map((p) => p.folder);
19241
19687
  const out = [];
19242
19688
  for (const folder of folders) {
19243
19689
  try {
19244
- const entries = await entriesFromDirectory(folder);
19690
+ const entries = await entriesFromDirectory(folder, MAX_SKILL_BYTES);
19245
19691
  const { skillId } = computeSkillId(entries);
19246
19692
  out.push({ folder, skillId });
19247
- } catch {
19693
+ } catch (e) {
19694
+ if (e instanceof SkillTooLargeError) {
19695
+ logger?.warn("Skill directory exceeds 50 MB limit; skipping", { folder });
19696
+ }
19248
19697
  }
19249
19698
  }
19250
19699
  return out;
19251
19700
  }
19252
19701
 
19702
+ // ../core/dist/skill-pending.js
19703
+ var import_promises10 = require("node:fs/promises");
19704
+ var import_node_path21 = require("node:path");
19705
+ init_config();
19706
+ init_file_utils();
19707
+ init_types2();
19708
+ var SCHEMA_VERSION2 = 1;
19709
+ var PENDING_TTL_MS = 60 * 60 * 1e3;
19710
+ function defaultPendingPath() {
19711
+ return (0, import_node_path21.join)(resolvePath(SAGE_DIR), "skill_pending.json");
19712
+ }
19713
+ async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nullLogger) {
19714
+ let raw;
19715
+ try {
19716
+ raw = await getFileContent(pendingPath);
19717
+ } catch {
19718
+ return { entries: {} };
19719
+ }
19720
+ try {
19721
+ const data = JSON.parse(raw);
19722
+ const rawEntries = data.entries ?? {};
19723
+ const now = Date.now();
19724
+ const entries = {};
19725
+ for (const [skillId, entry] of Object.entries(rawEntries)) {
19726
+ const folder = entry.folder;
19727
+ const submittedAt = entry.submitted_at;
19728
+ if (!folder || !submittedAt)
19729
+ continue;
19730
+ const ts = Date.parse(submittedAt);
19731
+ if (!isFreshTimestamp(ts, PENDING_TTL_MS, now))
19732
+ continue;
19733
+ entries[skillId] = {
19734
+ folder,
19735
+ submittedAt,
19736
+ agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
19737
+ containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
19738
+ };
19739
+ }
19740
+ return { entries };
19741
+ } catch (e) {
19742
+ logger.warn(`Failed to load pending marker from ${pendingPath}`, { error: String(e) });
19743
+ return { entries: {} };
19744
+ }
19745
+ }
19746
+ async function savePendingMarker(marker, pendingPath = defaultPendingPath(), logger = nullLogger) {
19747
+ if (Object.keys(marker.entries).length === 0) {
19748
+ try {
19749
+ await (0, import_promises10.unlink)(pendingPath);
19750
+ } catch (e) {
19751
+ if (!e || typeof e !== "object" || e.code !== "ENOENT") {
19752
+ logger.warn(`Failed to delete empty pending marker at ${pendingPath}`, {
19753
+ error: String(e)
19754
+ });
19755
+ }
19756
+ }
19757
+ return;
19758
+ }
19759
+ try {
19760
+ const data = {
19761
+ schema_version: SCHEMA_VERSION2,
19762
+ entries: Object.fromEntries(Object.entries(marker.entries).map(([skillId, entry]) => [
19763
+ skillId,
19764
+ {
19765
+ folder: entry.folder,
19766
+ submitted_at: entry.submittedAt,
19767
+ agent_runtime: entry.agentRuntime,
19768
+ container_key: entry.containerKey
19769
+ }
19770
+ ]))
19771
+ };
19772
+ await atomicWriteJson(pendingPath, data);
19773
+ } catch (e) {
19774
+ logger.warn(`Failed to save pending marker to ${pendingPath}`, { error: String(e) });
19775
+ }
19776
+ }
19777
+ function isPending(marker, skillId) {
19778
+ return skillId in marker.entries;
19779
+ }
19780
+ function addPending(marker, skillId, folder, origin = {}) {
19781
+ marker.entries[skillId] = {
19782
+ folder,
19783
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
19784
+ agentRuntime: origin.agentRuntime,
19785
+ containerKey: origin.containerKey
19786
+ };
19787
+ }
19788
+ function removePending(marker, skillId) {
19789
+ delete marker.entries[skillId];
19790
+ }
19791
+ function listPending(marker) {
19792
+ return Object.entries(marker.entries).map(([skillId, entry]) => ({
19793
+ skillId,
19794
+ folder: entry.folder,
19795
+ agentRuntime: entry.agentRuntime,
19796
+ containerKey: entry.containerKey
19797
+ }));
19798
+ }
19799
+
19800
+ // ../core/dist/skill-verdict-cache.js
19801
+ var import_node_path22 = require("node:path");
19802
+ init_config();
19803
+ init_file_utils();
19804
+ init_types2();
19805
+ var SCHEMA_VERSION3 = 1;
19806
+ var DEFAULT_VERDICT_TTL_MS = MS_PER_DAY;
19807
+ function parseSources(raw) {
19808
+ if (!Array.isArray(raw))
19809
+ return void 0;
19810
+ const sources = [];
19811
+ for (const item of raw) {
19812
+ if (!item || typeof item !== "object")
19813
+ continue;
19814
+ const entry = item;
19815
+ const source = {
19816
+ agentRuntime: typeof entry.agent_runtime === "string" ? entry.agent_runtime : void 0,
19817
+ containerKey: typeof entry.container_key === "string" ? entry.container_key : void 0
19818
+ };
19819
+ if (source.agentRuntime || source.containerKey)
19820
+ sources.push(source);
19821
+ }
19822
+ return sources.length > 0 ? sources : void 0;
19823
+ }
19824
+ function mergeSources(existing, incoming) {
19825
+ const merged = [];
19826
+ const seen = /* @__PURE__ */ new Set();
19827
+ for (const source of [...existing ?? [], ...incoming ?? []]) {
19828
+ const key = source.agentRuntime ?? "";
19829
+ if (seen.has(key))
19830
+ continue;
19831
+ seen.add(key);
19832
+ merged.push(source);
19833
+ }
19834
+ return merged.length > 0 ? merged : void 0;
19835
+ }
19836
+ function defaultCachePath2() {
19837
+ return (0, import_node_path22.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
19838
+ }
19839
+ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger = nullLogger) {
19840
+ let raw;
19841
+ try {
19842
+ raw = await getFileContent(cachePath);
19843
+ } catch {
19844
+ return { entries: {} };
19845
+ }
19846
+ try {
19847
+ const data = JSON.parse(raw);
19848
+ const rawEntries = data.entries ?? {};
19849
+ const now = Date.now();
19850
+ const entries = {};
19851
+ let pruned = 0;
19852
+ for (const [skillId, entry] of Object.entries(rawEntries)) {
19853
+ const analyzedAt = entry.analyzed_at;
19854
+ if (!analyzedAt)
19855
+ continue;
19856
+ const ts = Date.parse(analyzedAt);
19857
+ if (!isFreshTimestamp(ts, ttlMs, now)) {
19858
+ pruned++;
19859
+ continue;
19860
+ }
19861
+ entries[skillId] = {
19862
+ verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
19863
+ summary: typeof entry.summary === "string" ? entry.summary : void 0,
19864
+ skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
19865
+ sources: parseSources(entry.sources),
19866
+ analyzedAt
19867
+ };
19868
+ }
19869
+ const cache2 = { entries };
19870
+ if (pruned > 0) {
19871
+ logger.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
19872
+ await saveSkillVerdictCache(cache2, cachePath, logger);
19873
+ }
19874
+ return cache2;
19875
+ } catch (e) {
19876
+ logger.warn(`Failed to load verdict cache from ${cachePath}`, { error: String(e) });
19877
+ return { entries: {} };
19878
+ }
19879
+ }
19880
+ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger = nullLogger) {
19881
+ try {
19882
+ const data = {
19883
+ schema_version: SCHEMA_VERSION3,
19884
+ entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
19885
+ skillId,
19886
+ {
19887
+ verdict: v.verdict,
19888
+ summary: v.summary,
19889
+ skill_name: v.skillName,
19890
+ sources: v.sources?.map((s) => ({
19891
+ agent_runtime: s.agentRuntime,
19892
+ container_key: s.containerKey
19893
+ })),
19894
+ analyzed_at: v.analyzedAt
19895
+ }
19896
+ ]))
19897
+ };
19898
+ await atomicWriteJson(cachePath, data);
19899
+ } catch (e) {
19900
+ logger.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
19901
+ }
19902
+ }
19903
+ function getVerdict(cache2, skillId, ttlMs = DEFAULT_VERDICT_TTL_MS) {
19904
+ const entry = cache2.entries[skillId];
19905
+ if (!entry)
19906
+ return null;
19907
+ const ts = Date.parse(entry.analyzedAt);
19908
+ if (!isFreshTimestamp(ts, ttlMs))
19909
+ return null;
19910
+ return entry;
19911
+ }
19912
+ function putVerdict(cache2, skillId, verdict) {
19913
+ const existing = cache2.entries[skillId];
19914
+ cache2.entries[skillId] = {
19915
+ ...verdict,
19916
+ sources: mergeSources(existing?.sources, verdict.sources),
19917
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
19918
+ };
19919
+ }
19920
+
19253
19921
  // ../core/dist/plugin-scanner.js
19254
19922
  init_types2();
19255
19923
  var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
@@ -19274,15 +19942,34 @@ var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
19274
19942
  var MAX_FILE_SIZE = 512 * 1024;
19275
19943
  async function walkPluginFiles(installPath, logger) {
19276
19944
  const files = [];
19945
+ const visited = /* @__PURE__ */ new Set();
19946
+ let rootReal;
19947
+ try {
19948
+ rootReal = await (0, import_promises11.realpath)((0, import_node_path23.resolve)(installPath));
19949
+ } catch (e) {
19950
+ logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
19951
+ return files;
19952
+ }
19277
19953
  async function walk(dirOrFile) {
19954
+ let real;
19955
+ try {
19956
+ real = await (0, import_promises11.realpath)(dirOrFile);
19957
+ } catch {
19958
+ return;
19959
+ }
19960
+ if (visited.has(real))
19961
+ return;
19962
+ if (!isContained(real, rootReal))
19963
+ return;
19964
+ visited.add(real);
19278
19965
  let stats;
19279
19966
  try {
19280
- stats = await (0, import_promises9.stat)(dirOrFile);
19967
+ stats = await (0, import_promises11.stat)(dirOrFile);
19281
19968
  } catch {
19282
19969
  return;
19283
19970
  }
19284
19971
  if (stats.isFile()) {
19285
- if (SCANNABLE_EXTENSIONS2.has((0, import_node_path18.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
19972
+ if (SCANNABLE_EXTENSIONS2.has((0, import_node_path23.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
19286
19973
  files.push(dirOrFile);
19287
19974
  }
19288
19975
  return;
@@ -19290,32 +19977,41 @@ async function walkPluginFiles(installPath, logger) {
19290
19977
  if (stats.isDirectory()) {
19291
19978
  let entries;
19292
19979
  try {
19293
- entries = await (0, import_promises9.readdir)(dirOrFile);
19980
+ entries = await (0, import_promises11.readdir)(dirOrFile);
19294
19981
  } catch {
19295
19982
  return;
19296
19983
  }
19297
19984
  for (const entry of entries) {
19298
19985
  if (SKIP_DIRS.has(entry))
19299
19986
  continue;
19300
- const fullPath = (0, import_node_path18.join)(dirOrFile, entry);
19987
+ const fullPath = (0, import_node_path23.join)(dirOrFile, entry);
19301
19988
  await walk(fullPath);
19302
19989
  }
19303
19990
  }
19304
19991
  }
19305
19992
  try {
19306
- await walk(installPath);
19993
+ await walk((0, import_node_path23.resolve)(installPath));
19307
19994
  } catch (e) {
19308
19995
  logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
19309
19996
  }
19310
19997
  return files;
19311
19998
  }
19312
19999
  async function scanPlugin(plugin, options = {}) {
19313
- const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger } = options;
20000
+ const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger, skillVerdictCachePath, skillPendingPath, loggingConfig, skillVerdictTtlMs, uploadEnabled = true, deferUnknownSkills = false, agentRuntime } = options;
19314
20001
  const result = { plugin, findings: [] };
19315
- const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger) : Promise.resolve();
20002
+ const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger, {
20003
+ verdictCachePath: skillVerdictCachePath,
20004
+ pendingPath: skillPendingPath,
20005
+ loggingConfig,
20006
+ verdictTtlMs: skillVerdictTtlMs,
20007
+ uploadEnabled,
20008
+ deferUnknownSkills,
20009
+ agentRuntime
20010
+ }) : Promise.resolve(false);
19316
20011
  const files = await walkPluginFiles(plugin.installPath, logger);
19317
20012
  if (files.length === 0) {
19318
- await skillCheckPromise;
20013
+ if (await skillCheckPromise)
20014
+ result.deferCache = true;
19319
20015
  return result;
19320
20016
  }
19321
20017
  const allUrls = [];
@@ -19331,7 +20027,7 @@ async function scanPlugin(plugin, options = {}) {
19331
20027
  }
19332
20028
  if (amsiClient) {
19333
20029
  try {
19334
- const scanName = `${plugin.key}/${(0, import_node_path18.relative)(plugin.installPath, filePath)}`;
20030
+ const scanName = `${plugin.key}/${(0, import_node_path23.relative)(plugin.installPath, filePath)}`;
19335
20031
  const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
19336
20032
  if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
19337
20033
  result.findings.push({
@@ -19339,7 +20035,7 @@ async function scanPlugin(plugin, options = {}) {
19339
20035
  title: `AMSI detection (result=${amsiResult.amsiResult})`,
19340
20036
  severity: "critical",
19341
20037
  artifact: content.slice(0, 200),
19342
- sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
20038
+ sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
19343
20039
  });
19344
20040
  }
19345
20041
  } catch {
@@ -19392,7 +20088,7 @@ async function scanPlugin(plugin, options = {}) {
19392
20088
  title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
19393
20089
  severity: "critical",
19394
20090
  artifact: fr.sha256,
19395
- sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
20091
+ sourceFile: (0, import_node_path23.relative)(plugin.installPath, filePath)
19396
20092
  });
19397
20093
  }
19398
20094
  }
@@ -19400,46 +20096,109 @@ async function scanPlugin(plugin, options = {}) {
19400
20096
  } catch {
19401
20097
  }
19402
20098
  })() : Promise.resolve();
19403
- await Promise.all([urlCheckPromise, fileCheckPromise, skillCheckPromise]);
20099
+ const [, , skillDeferCache] = await Promise.all([
20100
+ urlCheckPromise,
20101
+ fileCheckPromise,
20102
+ skillCheckPromise
20103
+ ]);
20104
+ if (skillDeferCache)
20105
+ result.deferCache = true;
19404
20106
  return result;
19405
20107
  }
19406
- async function runSkillCheck(plugin, findings, logger) {
20108
+ function pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict) {
20109
+ const risk = (verdict.verdict ?? "").toUpperCase();
20110
+ if (risk !== "HIGH" && risk !== "CRITICAL")
20111
+ return;
20112
+ const severity = risk === "CRITICAL" ? "critical" : "warning";
20113
+ findings.push({
20114
+ threatId: "SKILL_CHECK",
20115
+ title: verdict.summary?.trim() || (risk === "CRITICAL" ? "Malicious skill detected" : "Suspicious skill detected"),
20116
+ severity,
20117
+ artifact: skillId.slice(0, 16),
20118
+ sourceFile: (0, import_node_path23.relative)(plugin.installPath, folder) || "."
20119
+ });
20120
+ }
20121
+ async function runSkillCheck(plugin, findings, logger, paths = {}) {
19407
20122
  try {
19408
- const skills = await computeSkillIdsForRoot(plugin.installPath);
20123
+ const skills = await computeSkillIdsForRoot(plugin.installPath, logger);
19409
20124
  if (skills.length === 0)
19410
- return;
19411
- const ids = skills.map((s) => s.skillId);
20125
+ return false;
20126
+ const verdictCache = await loadSkillVerdictCache(paths.verdictCachePath, paths.verdictTtlMs, logger);
20127
+ const uncached = [];
20128
+ for (const skill of skills) {
20129
+ const cached = getVerdict(verdictCache, skill.skillId, paths.verdictTtlMs);
20130
+ if (cached) {
20131
+ pushSkillFindingIfRisky(findings, plugin, skill.skillId, skill.folder, cached);
20132
+ } else {
20133
+ uncached.push(skill);
20134
+ }
20135
+ }
20136
+ if (uncached.length === 0)
20137
+ return false;
19412
20138
  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)
19417
- continue;
19418
- const risk = (verdict.overallRiskLevel ?? "").toUpperCase();
19419
- if (risk !== "HIGH" && risk !== "CRITICAL")
20139
+ const verdicts = await client.checkSkills(uncached.map((s) => s.skillId));
20140
+ const marker = await loadPendingMarker(paths.pendingPath, logger);
20141
+ let cacheModified = false;
20142
+ let markerModified = false;
20143
+ let lookupFailed = false;
20144
+ let graceDeferred = false;
20145
+ for (const { folder, skillId } of uncached) {
20146
+ if (!verdicts.has(skillId)) {
20147
+ lookupFailed = true;
19420
20148
  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
- });
20149
+ }
20150
+ const verdict = verdicts.get(skillId) ?? null;
20151
+ if (verdict) {
20152
+ putVerdict(verdictCache, skillId, {
20153
+ verdict: verdict.verdict,
20154
+ summary: verdict.summary,
20155
+ skillName: (0, import_node_path23.basename)(folder),
20156
+ sources: [{ agentRuntime: paths.agentRuntime, containerKey: plugin.key }]
20157
+ });
20158
+ cacheModified = true;
20159
+ pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict);
20160
+ if (isPending(marker, skillId)) {
20161
+ removePending(marker, skillId);
20162
+ markerModified = true;
20163
+ }
20164
+ } else if (!isPending(marker, skillId)) {
20165
+ if (paths.uploadEnabled === false) {
20166
+ if (paths.deferUnknownSkills)
20167
+ graceDeferred = true;
20168
+ } else {
20169
+ addPending(marker, skillId, folder, {
20170
+ agentRuntime: paths.agentRuntime,
20171
+ containerKey: plugin.key
20172
+ });
20173
+ markerModified = true;
20174
+ if (paths.loggingConfig) {
20175
+ logSkillQueued(paths.loggingConfig, skillId, plugin.key, folder).catch(() => {
20176
+ });
20177
+ }
20178
+ }
20179
+ }
19430
20180
  }
20181
+ if (cacheModified)
20182
+ await saveSkillVerdictCache(verdictCache, paths.verdictCachePath, logger);
20183
+ if (markerModified)
20184
+ await savePendingMarker(marker, paths.pendingPath, logger);
20185
+ const anyStillPending = uncached.some(({ skillId }) => isPending(marker, skillId));
20186
+ return markerModified || anyStillPending || lookupFailed || graceDeferred;
19431
20187
  } catch (e) {
19432
20188
  logger.debug("Skill check failed", { error: String(e) });
20189
+ return false;
19433
20190
  }
19434
20191
  }
19435
20192
 
19436
20193
  // ../core/dist/session-start.js
19437
20194
  var import_node_child_process2 = require("node:child_process");
19438
20195
  var import_node_fs4 = require("node:fs");
20196
+ var import_node_path25 = require("node:path");
19439
20197
  init_config();
19440
20198
  init_file_utils();
19441
20199
 
19442
20200
  // ../core/dist/session-start-scan.js
20201
+ var import_node_path24 = require("node:path");
19443
20202
  init_config();
19444
20203
  init_types2();
19445
20204
  function fromCachedFinding(finding) {
@@ -19522,6 +20281,9 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19522
20281
  deniedByException: 0,
19523
20282
  findingsCount: 0
19524
20283
  };
20284
+ const skillPendingPath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_pending.json") : void 0;
20285
+ const skillVerdictCachePath = context.sageDirPath ? (0, import_node_path24.join)(context.sageDirPath, "skill_verdict_cache.json") : void 0;
20286
+ const cacheTtlMs = skillCacheTtlMs(config2);
19525
20287
  for (const plugin of plugins) {
19526
20288
  const denyMatch = findPluginDenyException(exceptions, plugin.key);
19527
20289
  if (denyMatch) {
@@ -19546,7 +20308,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19546
20308
  stats.allowedByException += 1;
19547
20309
  continue;
19548
20310
  }
19549
- const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
20311
+ const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated, cacheTtlMs);
19550
20312
  if (cached && cached.findings.length === 0) {
19551
20313
  stats.cacheHitsClean += 1;
19552
20314
  continue;
@@ -19565,11 +20327,23 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19565
20327
  const result = await scanPlugin(plugin, {
19566
20328
  checkUrls: context.checkUrls ?? true,
19567
20329
  checkFileHashes: context.checkFileHashes ?? true,
20330
+ checkSkills: config2.skill_check.enabled,
20331
+ uploadEnabled: context.uploadUnknownSkills ?? config2.skill_check.upload_enabled,
20332
+ deferUnknownSkills: context.deferUnknownSkills,
19568
20333
  amsiClient,
19569
- logger
20334
+ logger,
20335
+ skillPendingPath,
20336
+ skillVerdictCachePath,
20337
+ skillVerdictTtlMs: cacheTtlMs,
20338
+ loggingConfig: config2.logging,
20339
+ agentRuntime: context.agentRuntime
19570
20340
  });
19571
- storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
19572
- cacheModified = true;
20341
+ if (result.deferCache) {
20342
+ logger.debug("Plugin cache deferred (skill verdict pending)", { plugin: plugin.key });
20343
+ } else {
20344
+ storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
20345
+ cacheModified = true;
20346
+ }
19573
20347
  if (result.findings.length > 0) {
19574
20348
  stats.findingsCount += result.findings.length;
19575
20349
  resultsWithFindings.push(result);
@@ -19596,6 +20370,7 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
19596
20370
  init_types2();
19597
20371
 
19598
20372
  // ../core/dist/version-check.js
20373
+ init_config();
19599
20374
  init_types2();
19600
20375
  var DEFAULT_TIMEOUT_MS3 = 5e3;
19601
20376
  function isNewerVersion(current, latest) {
@@ -19622,11 +20397,20 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
19622
20397
  logger.debug("Skipping version check: missing installation id");
19623
20398
  return null;
19624
20399
  }
20400
+ let config2 = context.config;
20401
+ if (!config2) {
20402
+ try {
20403
+ config2 = await loadConfig(context.configPath, logger);
20404
+ } catch (err) {
20405
+ logger.debug(`Version check config load failed: ${err}`);
20406
+ }
20407
+ }
19625
20408
  const envelope = buildSageProxyEnvelope({
19626
20409
  iid: context.iid,
19627
20410
  versionApp: currentVersion,
19628
20411
  agentRuntime: context.agentRuntime,
19629
- agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown"
20412
+ agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown",
20413
+ config: config2
19630
20414
  });
19631
20415
  const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
19632
20416
  const enriched = mergeExtendedInfo(envelope, extendedInfo);
@@ -19666,6 +20450,8 @@ async function runSessionStart(ctx) {
19666
20450
  const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
19667
20451
  pruneOrphanedTmpFiles(sageDirPath).catch(() => {
19668
20452
  });
20453
+ deployConfigDefaults(sageDirPath, logger).catch(() => {
20454
+ });
19669
20455
  maybeSpawnModelDownloadWorker({
19670
20456
  sageDirPath,
19671
20457
  workerPath: ctx.modelDownloadWorkerPath,
@@ -19676,6 +20462,13 @@ async function runSessionStart(ctx) {
19676
20462
  logger
19677
20463
  }).catch(() => {
19678
20464
  });
20465
+ const rollout = await resolveSkillUploadRollout({
20466
+ sageDirPath,
20467
+ configPath: ctx.configPath,
20468
+ version: ctx.version,
20469
+ logger,
20470
+ commitNotice: ctx.commitUploadNotice ?? true
20471
+ });
19679
20472
  const iidPromise = getInstallationId(sageDirPath).catch(() => void 0);
19680
20473
  const [scanResults, versionCheck] = await Promise.all([
19681
20474
  runSessionStartScan({
@@ -19687,7 +20480,11 @@ async function runSessionStart(ctx) {
19687
20480
  configPath: ctx.configPath,
19688
20481
  scanCachePath: ctx.scanCachePath,
19689
20482
  checkUrls: ctx.checkUrls,
19690
- checkFileHashes: ctx.checkFileHashes
20483
+ checkFileHashes: ctx.checkFileHashes,
20484
+ sageDirPath,
20485
+ agentRuntime: ctx.agentRuntime,
20486
+ uploadUnknownSkills: rollout.uploadActive,
20487
+ deferUnknownSkills: rollout.deferUnknownSkills
19691
20488
  }),
19692
20489
  iidPromise.then((iid) => {
19693
20490
  if (!iid) {
@@ -19696,11 +20493,25 @@ async function runSessionStart(ctx) {
19696
20493
  return checkForUpdate(ctx.version, logger, void 0, {
19697
20494
  agentRuntime: ctx.agentRuntime,
19698
20495
  agentRuntimeVersion: ctx.agentRuntimeVersion,
19699
- iid
20496
+ iid,
20497
+ configPath: ctx.configPath
19700
20498
  });
19701
20499
  }).catch(() => null)
19702
20500
  ]);
19703
- return { scanResults, versionCheck };
20501
+ maybeSpawnSkillUploadWorker({
20502
+ sageDirPath,
20503
+ workerPath: ctx.skillUploadWorkerPath,
20504
+ configPath: ctx.configPath,
20505
+ agentRuntime: ctx.agentRuntime,
20506
+ uploadActive: rollout.uploadActive,
20507
+ logger
20508
+ }).catch(() => {
20509
+ });
20510
+ return {
20511
+ scanResults,
20512
+ versionCheck,
20513
+ notices: rollout.showNotice ? [SKILL_UPLOAD_NOTICE.id] : []
20514
+ };
19704
20515
  }
19705
20516
  async function maybeSpawnModelDownloadWorker(args) {
19706
20517
  if (!args.workerPath) {
@@ -19746,6 +20557,7 @@ async function maybeSpawnModelDownloadWorker(args) {
19746
20557
  const spawned = spawnModelDownloadWorker({
19747
20558
  sageDirPath: args.sageDirPath,
19748
20559
  workerPath: args.workerPath,
20560
+ configPath: args.configPath,
19749
20561
  agentRuntime: args.agentRuntime,
19750
20562
  agentRuntimeVersion: args.agentRuntimeVersion,
19751
20563
  versionApp: args.versionApp,
@@ -19757,6 +20569,9 @@ async function maybeSpawnModelDownloadWorker(args) {
19757
20569
  missingModels
19758
20570
  });
19759
20571
  }
20572
+ function bunRuntimeEnv() {
20573
+ return process.versions.bun ? { BUN_BE_BUN: "1" } : {};
20574
+ }
19760
20575
  function spawnModelDownloadWorker(args) {
19761
20576
  const logger = args.logger ?? nullLogger;
19762
20577
  try {
@@ -19765,7 +20580,9 @@ function spawnModelDownloadWorker(args) {
19765
20580
  stdio: "ignore",
19766
20581
  env: {
19767
20582
  ...process.env,
20583
+ ...bunRuntimeEnv(),
19768
20584
  SAGE_DIR: args.sageDirPath,
20585
+ SAGE_CONFIG_PATH: args.configPath ?? "",
19769
20586
  SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown"),
19770
20587
  SAGE_AGENT_RUNTIME_VERSION: args.agentRuntimeVersion ?? "",
19771
20588
  SAGE_VERSION_APP: args.versionApp ?? "",
@@ -19779,9 +20596,83 @@ function spawnModelDownloadWorker(args) {
19779
20596
  return false;
19780
20597
  }
19781
20598
  }
20599
+ async function maybeSpawnSkillUploadWorker(args) {
20600
+ if (!args.uploadActive) {
20601
+ args.logger.debug("Skill upload worker skipped", {
20602
+ result: "skipped",
20603
+ skippedReason: "upload_inactive"
20604
+ });
20605
+ return;
20606
+ }
20607
+ try {
20608
+ const config2 = await loadConfig(args.configPath, args.logger);
20609
+ if (!config2.skill_check.enabled) {
20610
+ args.logger.debug("Skill upload worker skipped", {
20611
+ result: "skipped",
20612
+ skippedReason: "skill_check_disabled"
20613
+ });
20614
+ return;
20615
+ }
20616
+ } catch {
20617
+ }
20618
+ if (!args.workerPath) {
20619
+ args.logger.debug("Skill upload worker skipped", {
20620
+ result: "skipped",
20621
+ skippedReason: "missing_worker_path"
20622
+ });
20623
+ return;
20624
+ }
20625
+ if (!(0, import_node_fs4.existsSync)(args.workerPath)) {
20626
+ args.logger.debug("Skill upload worker skipped", {
20627
+ result: "skipped",
20628
+ skippedReason: "worker_script_not_found",
20629
+ workerPath: args.workerPath
20630
+ });
20631
+ return;
20632
+ }
20633
+ const marker = await loadPendingMarker((0, import_node_path25.join)(args.sageDirPath, "skill_pending.json"), args.logger);
20634
+ const pendingCount = listPending(marker).length;
20635
+ if (pendingCount === 0) {
20636
+ args.logger.debug("Skill upload worker skipped", {
20637
+ result: "skipped",
20638
+ skippedReason: "no_pending_skills"
20639
+ });
20640
+ return;
20641
+ }
20642
+ const spawned = spawnSkillUploadWorker({
20643
+ sageDirPath: args.sageDirPath,
20644
+ workerPath: args.workerPath,
20645
+ agentRuntime: args.agentRuntime,
20646
+ logger: args.logger
20647
+ });
20648
+ args.logger.debug("Skill upload worker spawn completed", {
20649
+ result: spawned ? "spawned" : "failed",
20650
+ pendingCount
20651
+ });
20652
+ }
20653
+ function spawnSkillUploadWorker(args) {
20654
+ const logger = args.logger ?? nullLogger;
20655
+ try {
20656
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [args.workerPath], {
20657
+ detached: true,
20658
+ stdio: "ignore",
20659
+ env: {
20660
+ ...process.env,
20661
+ ...bunRuntimeEnv(),
20662
+ SAGE_DIR: args.sageDirPath,
20663
+ SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown")
20664
+ }
20665
+ });
20666
+ child.unref();
20667
+ return true;
20668
+ } catch (err) {
20669
+ logger.warn("Failed to spawn skill upload worker", { error: String(err) });
20670
+ return false;
20671
+ }
20672
+ }
19782
20673
 
19783
20674
  // ../core/dist/scan-handler.js
19784
- async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion) {
20675
+ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose", agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans = true) {
19785
20676
  logger.debug(`${branding2.name} plugin scan started (${context})`, {
19786
20677
  agentRuntime,
19787
20678
  pluginsCount: plugins.length,
@@ -19796,8 +20687,20 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
19796
20687
  logger,
19797
20688
  agentRuntime,
19798
20689
  agentRuntimeVersion,
19799
- modelDownloadWorkerPath
20690
+ modelDownloadWorkerPath,
20691
+ skillUploadWorkerPath,
20692
+ commitUploadNotice
19800
20693
  });
20694
+ if (result.notices && result.notices.length > 0) {
20695
+ logger.debug(`${branding2.name} session-start produced notices`, {
20696
+ context,
20697
+ noticeIds: result.notices,
20698
+ forwarded: Boolean(onNotices)
20699
+ });
20700
+ onNotices?.(result.notices);
20701
+ } else {
20702
+ logger.debug(`${branding2.name} session-start produced no notices`, { context });
20703
+ }
19801
20704
  const findingsCount = result.scanResults.reduce((total, scanResult) => total + scanResult.findings.length, 0);
19802
20705
  const completionData = {
19803
20706
  agentRuntime,
@@ -19811,10 +20714,13 @@ async function runPluginScan(logger, context, plugins, threatsDir, trustedDomain
19811
20714
  } else {
19812
20715
  logger.debug(`${branding2.name} plugin scan (${context}) complete`, completionData);
19813
20716
  }
20717
+ if (findingsCount === 0 && !announceCleanScans) {
20718
+ return "";
20719
+ }
19814
20720
  return formatSessionStartMessage(version, result, branding2, style);
19815
20721
  }
19816
20722
  function createScanHandler(options) {
19817
- const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, trustedDomainsDir, version, agentRuntime, agentRuntimeVersion, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
20723
+ 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
20724
  return async () => {
19819
20725
  try {
19820
20726
  let plugins = await discoverPlugins2();
@@ -19822,8 +20728,10 @@ function createScanHandler(options) {
19822
20728
  if (plugins.length === 0) {
19823
20729
  logger.debug(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
19824
20730
  }
19825
- const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion);
19826
- onResult?.(msg);
20731
+ const msg = await runPluginScan(logger, context, plugins, threatsDir, trustedDomainsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style, agentRuntimeVersion, skillUploadWorkerPath, onNotices, commitUploadNotice, announceCleanScans);
20732
+ if (msg !== "") {
20733
+ onResult?.(msg);
20734
+ }
19827
20735
  } catch (e) {
19828
20736
  logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
19829
20737
  }
@@ -19861,19 +20769,19 @@ function canonicalizeToolName(map, rawName) {
19861
20769
  init_types2();
19862
20770
 
19863
20771
  // src/bundled-dirs.ts
19864
- var import_node_path19 = require("node:path");
19865
- var packageRoot = (0, import_node_path19.resolve)(__dirname, "..");
20772
+ var import_node_path26 = require("node:path");
20773
+ var packageRoot = (0, import_node_path26.resolve)(__dirname, "..");
19866
20774
  function getBundledDataDirs() {
19867
20775
  return {
19868
- threatsDir: (0, import_node_path19.join)(packageRoot, "resources", "threats"),
19869
- trustedDomainsDir: (0, import_node_path19.join)(packageRoot, "resources", "trusted-domains")
20776
+ threatsDir: (0, import_node_path26.join)(packageRoot, "resources", "threats"),
20777
+ trustedDomainsDir: (0, import_node_path26.join)(packageRoot, "resources", "trusted-domains")
19870
20778
  };
19871
20779
  }
19872
20780
  var cachedVersion;
19873
20781
  function getSageVersion() {
19874
20782
  if (cachedVersion) return cachedVersion;
19875
20783
  try {
19876
- const pkg = JSON.parse(getFileContentSync((0, import_node_path19.join)(packageRoot, "package.json"), "utf-8"));
20784
+ const pkg = JSON.parse(getFileContentSync((0, import_node_path26.join)(packageRoot, "package.json"), "utf-8"));
19877
20785
  cachedVersion = pkg.version ?? "0.0.0";
19878
20786
  } catch {
19879
20787
  cachedVersion = "0.0.0";
@@ -19882,37 +20790,41 @@ function getSageVersion() {
19882
20790
  }
19883
20791
 
19884
20792
  // src/startup-scan.ts
19885
- var import_node_path21 = require("node:path");
20793
+ var import_node_path28 = require("node:path");
19886
20794
 
19887
20795
  // src/plugin-discovery.ts
19888
- var import_promises10 = require("node:fs/promises");
20796
+ var import_promises12 = require("node:fs/promises");
19889
20797
  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) {
20798
+ var import_node_path27 = require("node:path");
20799
+ var DEFAULT_EXTENSIONS_DIR = (0, import_node_path27.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
20800
+ var SKILL_ROOTS = [
20801
+ { dir: (0, import_node_path27.join)(getHomeDir(), ".agents", "skills"), tag: "agents", scope: "personal" },
20802
+ { dir: (0, import_node_path27.join)(getHomeDir(), ".openclaw", "skills"), tag: "openclaw", scope: "personal" }
20803
+ ];
20804
+ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding, skillRoots = SKILL_ROOTS) {
19893
20805
  logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
19894
20806
  path: extensionsDir
19895
20807
  });
20808
+ const plugins = [];
19896
20809
  let entries;
19897
20810
  try {
19898
- entries = await (0, import_promises10.readdir)(extensionsDir);
20811
+ entries = await (0, import_promises12.readdir)(extensionsDir);
19899
20812
  } catch {
19900
20813
  logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
19901
- return [];
20814
+ entries = [];
19902
20815
  }
19903
- const plugins = [];
19904
20816
  for (const entry of entries) {
19905
- const extDir = (0, import_node_path20.join)(extensionsDir, entry);
20817
+ const extDir = (0, import_node_path27.join)(extensionsDir, entry);
19906
20818
  let stats;
19907
20819
  try {
19908
- stats = await (0, import_promises10.stat)(extDir);
20820
+ stats = await (0, import_promises12.stat)(extDir);
19909
20821
  if (!stats.isDirectory()) continue;
19910
20822
  } catch {
19911
20823
  continue;
19912
20824
  }
19913
20825
  let pkg;
19914
20826
  try {
19915
- const pkgPath = (0, import_node_path20.join)(extDir, "package.json");
20827
+ const pkgPath = (0, import_node_path27.join)(extDir, "package.json");
19916
20828
  const raw = await getFileContent(pkgPath);
19917
20829
  pkg = JSON.parse(raw);
19918
20830
  } catch {
@@ -19934,11 +20846,14 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
19934
20846
  });
19935
20847
  }
19936
20848
  logger.debug(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
20849
+ const skills = await discoverLooseSkillsAcrossRoots(skillRoots);
20850
+ logger.debug(`${branding2.name} skill discovery: found ${skills.length} loose skill(s)`);
20851
+ plugins.push(...skills);
19937
20852
  return plugins;
19938
20853
  }
19939
20854
 
19940
20855
  // src/startup-scan.ts
19941
- function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult) {
20856
+ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding, onResult, announceCleanScans = true) {
19942
20857
  const { threatsDir, trustedDomainsDir } = getBundledDataDirs();
19943
20858
  const version = getSageVersion();
19944
20859
  return createScanHandler({
@@ -19953,29 +20868,58 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
19953
20868
  // No agentRuntimeVersion: the OpenClaw plugin API exposes no host version.
19954
20869
  branding: branding2,
19955
20870
  onResult,
19956
- modelDownloadWorkerPath: (0, import_node_path21.resolve)(__dirname, "model-download-worker.cjs")
20871
+ // No onNotices: OpenClaw delivers the one-time notice from
20872
+ // `before_agent_start` (a different process/turn than this scan) via
20873
+ // takePendingNotices, so the scan-time callback would never fire.
20874
+ modelDownloadWorkerPath: (0, import_node_path28.resolve)(__dirname, "model-download-worker.cjs"),
20875
+ skillUploadWorkerPath: (0, import_node_path28.resolve)(__dirname, "skill-upload-worker.cjs"),
20876
+ // Don't commit the "shown" flag here — commit happens at delivery.
20877
+ commitUploadNotice: false,
20878
+ announceCleanScans
19957
20879
  });
19958
20880
  }
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 };
20881
+ function createStartupScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
20882
+ return createOpenClawScanHandler(logger, "startup", branding2, onResult, announceCleanScans);
20883
+ }
20884
+ function createSessionScanHandler(logger, branding2 = defaultBranding, onResult, announceCleanScans = true) {
20885
+ return createOpenClawScanHandler(logger, "session", branding2, onResult, announceCleanScans);
20886
+ }
20887
+ function createBeforeAgentStartHandler(getSecurityFindings, clearFindings, logger, branding2 = defaultBranding) {
20888
+ return (notices) => {
20889
+ const findings = getSecurityFindings();
20890
+ if (!notices && !findings) {
20891
+ logger.debug(`${branding2.name}: before_agent_start - nothing pending to surface`);
20892
+ return void 0;
20893
+ }
20894
+ if (findings) clearFindings();
20895
+ logger.debug(`${branding2.name}: surfacing context via before_agent_start`, {
20896
+ hasNotices: Boolean(notices),
20897
+ hasFindings: Boolean(findings)
20898
+ });
20899
+ const blocks = [];
20900
+ if (notices) {
20901
+ blocks.push(
20902
+ [
20903
+ '<sage-notice source="sage-session-start">',
20904
+ notices,
20905
+ "",
20906
+ "Relay this notice to the user. It is informational, not a security finding.",
20907
+ "</sage-notice>"
20908
+ ].join("\n")
20909
+ );
20910
+ }
20911
+ if (findings) {
20912
+ blocks.push(
20913
+ [
20914
+ '<security-alert source="sage-plugin-scan">',
20915
+ findings,
20916
+ "",
20917
+ "Inform the user about these security findings.",
20918
+ "</security-alert>"
20919
+ ].join("\n")
20920
+ );
20921
+ }
20922
+ return { prependContext: blocks.join("\n\n") };
19979
20923
  };
19980
20924
  }
19981
20925
 
@@ -20062,6 +21006,17 @@ function createToolCallHandler(approvalStore, logger, threatsDir, trustedDomains
20062
21006
  });
20063
21007
  try {
20064
21008
  const { toolName, params } = event;
21009
+ if (captureEnabled()) {
21010
+ await captureHookInput(
21011
+ "PreToolUse",
21012
+ { toolName, params, sessionKey: ctx?.sessionKey },
21013
+ {
21014
+ toolName: canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName),
21015
+ toolInput: normalizeToolInput(toolName, params)
21016
+ },
21017
+ { filePrefix: "openclaw-" }
21018
+ );
21019
+ }
20065
21020
  const artifacts = mapToolToArtifacts(toolName, params);
20066
21021
  if (!artifacts || artifacts.length === 0) {
20067
21022
  completeHook("skipped", { skippedReason: "no_artifacts" });
@@ -20187,9 +21142,9 @@ ${notice}` : notice;
20187
21142
  const onFindings = (msg) => {
20188
21143
  pendingScanFindings = msg;
20189
21144
  };
20190
- const getPendingFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
21145
+ const getPendingSecurityFindings = () => [pendingConfigurationWarnings, pendingScanFindings].filter(Boolean).join("\n\n") || null;
20191
21146
  const beforeAgentStartHandler = createBeforeAgentStartHandler(
20192
- getPendingFindings,
21147
+ getPendingSecurityFindings,
20193
21148
  () => {
20194
21149
  pendingConfigurationWarnings = null;
20195
21150
  pendingScanFindings = null;
@@ -20197,16 +21152,37 @@ ${notice}` : notice;
20197
21152
  logger,
20198
21153
  branding
20199
21154
  );
21155
+ const resolveNoticeText = async () => {
21156
+ try {
21157
+ const ids = await takePendingNotices({ logger });
21158
+ const text = ids.map((id) => formatNoticeById(id, branding)).filter(Boolean).join("\n\n");
21159
+ logger.debug(`${branding.name}: notices resolved for delivery`, {
21160
+ noticeIds: ids,
21161
+ willShow: text.length > 0
21162
+ });
21163
+ return text.length > 0 ? text : null;
21164
+ } catch (e) {
21165
+ logger.warn(`${branding.name}: notice resolution failed`, { error: String(e) });
21166
+ return null;
21167
+ }
21168
+ };
20200
21169
  api.on(
20201
21170
  "before_tool_call",
20202
21171
  createToolCallHandler(approvalStore, toolLogger, threatsDir, trustedDomainsDir, branding),
20203
21172
  { priority: 100 }
20204
21173
  );
20205
- api.on("gateway_start", createStartupScanHandler(scanLogger, branding, onFindings));
20206
- api.on("session_start", createSessionScanHandler(scanLogger, branding, onFindings));
21174
+ api.on(
21175
+ "gateway_start",
21176
+ createStartupScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
21177
+ );
21178
+ api.on(
21179
+ "session_start",
21180
+ createSessionScanHandler(scanLogger, branding, onFindings, config.announce_clean_scans)
21181
+ );
20207
21182
  api.on("before_agent_start", async () => {
20208
21183
  await migrationCheckPromise;
20209
- return beforeAgentStartHandler();
21184
+ const notices = await resolveNoticeText();
21185
+ return beforeAgentStartHandler(notices);
20210
21186
  });
20211
21187
  }
20212
21188
  };