@gendigital/sage-openclaw 0.8.0 → 0.9.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
@@ -82,6 +82,35 @@ async function pruneOrphanedTmpFiles(resolvedDir, maxAgeMs = 3e5) {
82
82
  }
83
83
  } catch {
84
84
  }
85
+ await pruneOrphanedModelDownloads((0, import_node_path.join)(resolvedDir, "models", ".download"));
86
+ }
87
+ async function pruneOrphanedModelDownloads(stagingDir, maxAgeMs = 60 * 60 * 1e3) {
88
+ let entries;
89
+ try {
90
+ entries = await fsPromises.readdir(stagingDir);
91
+ } catch {
92
+ return;
93
+ }
94
+ const now = Date.now();
95
+ for (const entry of entries) {
96
+ const fullPath = (0, import_node_path.join)(stagingDir, entry);
97
+ const isArchive = entry.endsWith(".tar.gz");
98
+ const isPartial = entry.endsWith(".partial");
99
+ const isStaging = entry.startsWith("stage-");
100
+ if (!isArchive && !isPartial && !isStaging)
101
+ continue;
102
+ try {
103
+ const s = await fsPromises.stat(fullPath);
104
+ if (now - s.mtimeMs <= maxAgeMs)
105
+ continue;
106
+ if (s.isDirectory()) {
107
+ await fsPromises.rm(fullPath, { recursive: true, force: true });
108
+ } else {
109
+ await fsPromises.unlink(fullPath);
110
+ }
111
+ } catch {
112
+ }
113
+ }
85
114
  }
86
115
  var import_node_crypto, fs, fsPromises, import_node_os, import_node_path, name1, name2;
87
116
  var init_file_utils = __esm({
@@ -4204,7 +4233,7 @@ var init_zod = __esm({
4204
4233
  });
4205
4234
 
4206
4235
  // ../core/dist/types.js
4207
- var nullLogger, ArtifactTypeSchema, ArtifactSchema, SeveritySchema, ActionSchema, ThreatSchema, DecisionSchema, VerdictSeveritySchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, AllowlistConfigSchema, LoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, brandString, BrandingSchema, HookTypeSchema;
4236
+ var nullLogger, ArtifactTypeSchema, ArtifactSchema, SeveritySchema, ActionSchema, ThreatSchema, DecisionSchema, VerdictSeveritySchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, AllowlistConfigSchema, LoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
4208
4237
  var init_types2 = __esm({
4209
4238
  "../core/dist/types.js"() {
4210
4239
  "use strict";
@@ -4278,6 +4307,15 @@ var init_types2 = __esm({
4278
4307
  AmsiCheckConfigSchema = external_exports.object({
4279
4308
  enabled: external_exports.boolean().default(true)
4280
4309
  });
4310
+ DEFAULT_PI_HIGH_RISK_THRESHOLD = 0.99;
4311
+ DEFAULT_PI_MEDIUM_RISK_THRESHOLD = 0.5;
4312
+ PiCheckConfigSchema = external_exports.object({
4313
+ enabled: external_exports.boolean().default(false),
4314
+ max_content_length: external_exports.number().default(16384),
4315
+ model_path: external_exports.string().optional(),
4316
+ high_risk_threshold: external_exports.number().default(DEFAULT_PI_HIGH_RISK_THRESHOLD),
4317
+ medium_risk_threshold: external_exports.number().default(DEFAULT_PI_MEDIUM_RISK_THRESHOLD)
4318
+ });
4281
4319
  ExceptionDecisionSchema = external_exports.enum(["allow", "deny"]);
4282
4320
  ExceptionMatchSchema = external_exports.enum(["executable", "domain", "path", "plugin", "regex"]);
4283
4321
  ExceptionRuleSchema = external_exports.object({
@@ -4298,6 +4336,7 @@ var init_types2 = __esm({
4298
4336
  file_check: FileCheckConfigSchema.default({}),
4299
4337
  package_check: PackageCheckConfigSchema.default({}),
4300
4338
  amsi_check: AmsiCheckConfigSchema.default({}),
4339
+ pi_check: PiCheckConfigSchema.default({}),
4301
4340
  heuristics_enabled: external_exports.boolean().default(true),
4302
4341
  cache: CacheConfigSchema.default({}),
4303
4342
  allowlist: AllowlistConfigSchema.default({}),
@@ -4305,17 +4344,9 @@ var init_types2 = __esm({
4305
4344
  logging: LoggingConfigSchema.default({}),
4306
4345
  sensitivity: SensitivitySchema.default("balanced"),
4307
4346
  disabled_threats: external_exports.array(external_exports.string()).default([]),
4347
+ brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
4308
4348
  community_iq: external_exports.boolean().default(true)
4309
4349
  });
4310
- brandString = external_exports.string().min(1).max(64).regex(/^[^\p{Cc}]+$/u, "No control characters");
4311
- BrandingSchema = external_exports.object({
4312
- product_name: brandString.default("Sage"),
4313
- banner_text: external_exports.string().min(1).max(128).regex(/^[^\p{Cc}]+$/u).optional(),
4314
- brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional()
4315
- }).transform((b) => ({
4316
- ...b,
4317
- banner_text: b.banner_text ?? b.product_name
4318
- }));
4319
4350
  HookTypeSchema = external_exports.enum([
4320
4351
  "PreToolUse",
4321
4352
  "PostToolUse",
@@ -4333,6 +4364,7 @@ __export(config_exports, {
4333
4364
  HOOK_TIMEOUT_SECONDS: () => HOOK_TIMEOUT_SECONDS,
4334
4365
  SAGE_DIR: () => SAGE_DIR,
4335
4366
  loadConfig: () => loadConfig,
4367
+ loadConfigSync: () => loadConfigSync,
4336
4368
  resolvePath: () => resolvePath
4337
4369
  });
4338
4370
  function resolvedSageDir() {
@@ -4396,28 +4428,39 @@ function normalizeStateFilePath(configuredPath, fallbackPath, field, logger) {
4396
4428
  });
4397
4429
  return fallbackPath;
4398
4430
  }
4399
- function sanitizeConfigPaths(config, logger) {
4431
+ function sanitizeBrandKey(data, logger) {
4432
+ const brandKey = data.brand_key;
4433
+ if (brandKey === void 0)
4434
+ return data;
4435
+ if (typeof brandKey === "string" && brandKey.length >= 1 && brandKey.length <= 32 && BRAND_KEY_RE.test(brandKey)) {
4436
+ return data;
4437
+ }
4438
+ logger.warn(`Invalid brand_key in config \u2014 ignoring`, { brand_key: brandKey });
4439
+ const { brand_key: _, ...rest } = data;
4440
+ return rest;
4441
+ }
4442
+ function sanitizeConfigPaths(config2, logger) {
4400
4443
  const cachePath = defaultCachePath();
4401
4444
  const allowlistPath = defaultAllowlistPath();
4402
4445
  const exceptionsPath = defaultExceptionsPath();
4403
4446
  const auditPath = defaultAuditPath();
4404
4447
  return {
4405
- ...config,
4448
+ ...config2,
4406
4449
  cache: {
4407
- ...config.cache,
4408
- path: normalizeStateFilePath(config.cache.path, cachePath, "cache", logger)
4450
+ ...config2.cache,
4451
+ path: normalizeStateFilePath(config2.cache.path, cachePath, "cache", logger)
4409
4452
  },
4410
4453
  allowlist: {
4411
- ...config.allowlist,
4412
- path: normalizeStateFilePath(config.allowlist.path, allowlistPath, "allowlist", logger)
4454
+ ...config2.allowlist,
4455
+ path: normalizeStateFilePath(config2.allowlist.path, allowlistPath, "allowlist", logger)
4413
4456
  },
4414
4457
  exceptions: {
4415
- ...config.exceptions,
4416
- path: normalizeStateFilePath(config.exceptions.path, exceptionsPath, "exceptions", logger)
4458
+ ...config2.exceptions,
4459
+ path: normalizeStateFilePath(config2.exceptions.path, exceptionsPath, "exceptions", logger)
4417
4460
  },
4418
4461
  logging: {
4419
- ...config.logging,
4420
- path: normalizeStateFilePath(config.logging.path, auditPath, "logging", logger)
4462
+ ...config2.logging,
4463
+ path: normalizeStateFilePath(config2.logging.path, auditPath, "logging", logger)
4421
4464
  }
4422
4465
  };
4423
4466
  }
@@ -4440,14 +4483,42 @@ async function loadConfig(configPath, logger = nullLogger) {
4440
4483
  logger.warn(`Config file ${path} does not contain a JSON object`);
4441
4484
  return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4442
4485
  }
4486
+ const sanitized = sanitizeBrandKey(data, logger);
4487
+ try {
4488
+ return sanitizeConfigPaths(ConfigSchema.parse(sanitized), logger);
4489
+ } catch (e) {
4490
+ logger.warn(`Config validation failed, using defaults`, { error: String(e) });
4491
+ return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4492
+ }
4493
+ }
4494
+ function loadConfigSync(configPath, logger = nullLogger) {
4495
+ const path = configPath ?? defaultConfigPath();
4496
+ let raw;
4497
+ try {
4498
+ raw = getFileContentSync(path);
4499
+ } catch {
4500
+ return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4501
+ }
4502
+ let data;
4503
+ try {
4504
+ data = JSON.parse(raw);
4505
+ } catch (e) {
4506
+ logger.warn(`Failed to parse config from ${path}`, { error: String(e) });
4507
+ return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4508
+ }
4509
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
4510
+ logger.warn(`Config file ${path} does not contain a JSON object`);
4511
+ return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4512
+ }
4513
+ const sanitized = sanitizeBrandKey(data, logger);
4443
4514
  try {
4444
- return sanitizeConfigPaths(ConfigSchema.parse(data), logger);
4515
+ return sanitizeConfigPaths(ConfigSchema.parse(sanitized), logger);
4445
4516
  } catch (e) {
4446
4517
  logger.warn(`Config validation failed, using defaults`, { error: String(e) });
4447
4518
  return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
4448
4519
  }
4449
4520
  }
4450
- var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS;
4521
+ var import_node_path2, SAGE_DIR, HOOK_TIMEOUT_SECONDS, BRAND_KEY_RE;
4451
4522
  var init_config = __esm({
4452
4523
  "../core/dist/config.js"() {
4453
4524
  "use strict";
@@ -4456,6 +4527,7 @@ var init_config = __esm({
4456
4527
  init_types2();
4457
4528
  SAGE_DIR = "~/.sage";
4458
4529
  HOOK_TIMEOUT_SECONDS = 8;
4530
+ BRAND_KEY_RE = /^[a-z0-9_-]+$/u;
4459
4531
  }
4460
4532
  });
4461
4533
 
@@ -5401,7 +5473,7 @@ var require_range = __commonJS({
5401
5473
  parseRange(range) {
5402
5474
  const memoOpts = (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) | (this.options.loose && FLAG_LOOSE);
5403
5475
  const memoKey = memoOpts + ":" + range;
5404
- const cached = cache.get(memoKey);
5476
+ const cached = cache2.get(memoKey);
5405
5477
  if (cached) {
5406
5478
  return cached;
5407
5479
  }
@@ -5435,7 +5507,7 @@ var require_range = __commonJS({
5435
5507
  rangeMap.delete("");
5436
5508
  }
5437
5509
  const result = [...rangeMap.values()];
5438
- cache.set(memoKey, result);
5510
+ cache2.set(memoKey, result);
5439
5511
  return result;
5440
5512
  }
5441
5513
  intersects(range, options) {
@@ -5474,7 +5546,7 @@ var require_range = __commonJS({
5474
5546
  };
5475
5547
  module2.exports = Range;
5476
5548
  var LRU = require_lrucache();
5477
- var cache = new LRU();
5549
+ var cache2 = new LRU();
5478
5550
  var parseOptions = require_parse_options();
5479
5551
  var Comparator = require_comparator();
5480
5552
  var debug = require_debug();
@@ -6389,6 +6461,363 @@ var require_semver2 = __commonJS({
6389
6461
  }
6390
6462
  });
6391
6463
 
6464
+ // ../core/dist/clients/tokenizer.js
6465
+ var tokenizer_exports = {};
6466
+ __export(tokenizer_exports, {
6467
+ LocalTokenizer: () => LocalTokenizer
6468
+ });
6469
+ function isWhitespace(ch) {
6470
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r")
6471
+ return true;
6472
+ return new RegExp("^\\p{Zs}$", "u").test(ch);
6473
+ }
6474
+ function isControl(ch) {
6475
+ if (ch === " " || ch === "\n" || ch === "\r")
6476
+ return false;
6477
+ return new RegExp("^\\p{Cc}|\\p{Cf}$", "u").test(ch);
6478
+ }
6479
+ function isPunctuation(ch) {
6480
+ const cp = ch.codePointAt(0) ?? 0;
6481
+ if (33 <= cp && cp <= 47 || 58 <= cp && cp <= 64 || 91 <= cp && cp <= 96 || 123 <= cp && cp <= 126) {
6482
+ return true;
6483
+ }
6484
+ return new RegExp("^\\p{P}$", "u").test(ch);
6485
+ }
6486
+ function isChineseChar(cp) {
6487
+ return 19968 <= cp && cp <= 40959 || 13312 <= cp && cp <= 19903 || 131072 <= cp && cp <= 173791 || 173824 <= cp && cp <= 177983 || 177984 <= cp && cp <= 178207 || 178208 <= cp && cp <= 183983 || 63744 <= cp && cp <= 64255 || 194560 <= cp && cp <= 195103;
6488
+ }
6489
+ function cleanText(text) {
6490
+ const chars = [];
6491
+ const posMap = [];
6492
+ for (let i = 0; i < text.length; i++) {
6493
+ const ch = text[i] ?? "";
6494
+ const cp = text.codePointAt(i) ?? 0;
6495
+ if (cp === 0 || cp === 65533 || isControl(ch))
6496
+ continue;
6497
+ chars.push(isWhitespace(ch) ? " " : ch);
6498
+ posMap.push(i);
6499
+ }
6500
+ return { chars, posMap };
6501
+ }
6502
+ function addChineseSpacing(chars, posMap) {
6503
+ const out = [];
6504
+ const newMap = [];
6505
+ for (let i = 0; i < chars.length; i++) {
6506
+ const ch = chars[i] ?? "";
6507
+ const pos = posMap[i] ?? 0;
6508
+ if (isChineseChar(ch.codePointAt(0) ?? 0)) {
6509
+ out.push(" ", ch, " ");
6510
+ newMap.push(pos, pos, pos);
6511
+ } else {
6512
+ out.push(ch);
6513
+ newMap.push(pos);
6514
+ }
6515
+ }
6516
+ return { chars: out, posMap: newMap };
6517
+ }
6518
+ function nfcNormalize(chars, posMap) {
6519
+ const out = [];
6520
+ const newMap = [];
6521
+ for (let i = 0; i < chars.length; i++) {
6522
+ const ch = chars[i] ?? "";
6523
+ const nfc = ch.normalize("NFC");
6524
+ for (const c of nfc) {
6525
+ out.push(c);
6526
+ newMap.push(posMap[i] ?? i);
6527
+ }
6528
+ }
6529
+ return { chars: out, posMap: newMap };
6530
+ }
6531
+ function lowercaseChars(chars, posMap) {
6532
+ return { chars: chars.map((ch) => ch.toLowerCase()), posMap };
6533
+ }
6534
+ function stripAccents(chars, posMap) {
6535
+ const out = [];
6536
+ const newMap = [];
6537
+ for (let i = 0; i < chars.length; i++) {
6538
+ const ch = chars[i] ?? "";
6539
+ const nfd = ch.normalize("NFD");
6540
+ for (const c of nfd) {
6541
+ if (!new RegExp("^\\p{Mn}$", "u").test(c)) {
6542
+ out.push(c);
6543
+ newMap.push(posMap[i] ?? i);
6544
+ }
6545
+ }
6546
+ }
6547
+ return { chars: out, posMap: newMap };
6548
+ }
6549
+ function whitespaceSplit(chars, posMap) {
6550
+ const words = [];
6551
+ let currentChars = [];
6552
+ let currentPos = [];
6553
+ for (let i = 0; i < chars.length; i++) {
6554
+ if (chars[i] === " ") {
6555
+ if (currentChars.length > 0) {
6556
+ words.push({ word: currentChars.join(""), positions: currentPos });
6557
+ currentChars = [];
6558
+ currentPos = [];
6559
+ }
6560
+ } else {
6561
+ currentChars.push(chars[i] ?? "");
6562
+ currentPos.push(posMap[i] ?? i);
6563
+ }
6564
+ }
6565
+ if (currentChars.length > 0) {
6566
+ words.push({ word: currentChars.join(""), positions: currentPos });
6567
+ }
6568
+ return words;
6569
+ }
6570
+ function splitOnPunctuation(word, positions) {
6571
+ const tokens = [];
6572
+ let currentChars = [];
6573
+ let currentPos = [];
6574
+ for (let i = 0; i < word.length; i++) {
6575
+ const ch = word[i] ?? "";
6576
+ const pos = positions[i] ?? i;
6577
+ if (isPunctuation(ch)) {
6578
+ if (currentChars.length > 0) {
6579
+ tokens.push({ word: currentChars.join(""), positions: currentPos });
6580
+ currentChars = [];
6581
+ currentPos = [];
6582
+ }
6583
+ tokens.push({ word: ch, positions: [pos] });
6584
+ } else {
6585
+ currentChars.push(ch);
6586
+ currentPos.push(pos);
6587
+ }
6588
+ }
6589
+ if (currentChars.length > 0) {
6590
+ tokens.push({ word: currentChars.join(""), positions: currentPos });
6591
+ }
6592
+ return tokens;
6593
+ }
6594
+ function pretokenize(text) {
6595
+ if (!text)
6596
+ return [];
6597
+ let { chars, posMap } = cleanText(text);
6598
+ ({ chars, posMap } = addChineseSpacing(chars, posMap));
6599
+ ({ chars, posMap } = nfcNormalize(chars, posMap));
6600
+ const words = whitespaceSplit(chars, posMap);
6601
+ const result = [];
6602
+ for (const { word, positions } of words) {
6603
+ let wChars = [...word];
6604
+ let wPos = [...positions];
6605
+ ({ chars: wChars, posMap: wPos } = lowercaseChars(wChars, wPos));
6606
+ ({ chars: wChars, posMap: wPos } = stripAccents(wChars, wPos));
6607
+ const subTokens = splitOnPunctuation(wChars.join(""), wPos);
6608
+ result.push(...subTokens);
6609
+ }
6610
+ return result;
6611
+ }
6612
+ function tokenizeSubwords(word, vocab, unkId) {
6613
+ if (word.length > MAX_WORD_CHARS) {
6614
+ return { tokens: ["[UNK]"], ids: [unkId], spans: [[0, word.length]] };
6615
+ }
6616
+ const tokens = [];
6617
+ const ids = [];
6618
+ const spans = [];
6619
+ let start = 0;
6620
+ while (start < word.length) {
6621
+ let end = word.length;
6622
+ let found = null;
6623
+ let foundId = -1;
6624
+ while (start < end) {
6625
+ let substr = word.slice(start, end);
6626
+ if (start > 0)
6627
+ substr = `##${substr}`;
6628
+ const id = vocab.get(substr);
6629
+ if (id !== void 0) {
6630
+ found = substr;
6631
+ foundId = id;
6632
+ break;
6633
+ }
6634
+ end--;
6635
+ }
6636
+ if (found === null) {
6637
+ return { tokens: ["[UNK]"], ids: [unkId], spans: [[0, word.length]] };
6638
+ }
6639
+ tokens.push(found);
6640
+ ids.push(foundId);
6641
+ spans.push([start, end]);
6642
+ start = end;
6643
+ }
6644
+ return { tokens, ids, spans };
6645
+ }
6646
+ var import_node_fs2, import_node_path7, MAX_WORD_CHARS, PAD_ID, UNK_ID, CLS_ID, SEP_ID, LocalTokenizer;
6647
+ var init_tokenizer = __esm({
6648
+ "../core/dist/clients/tokenizer.js"() {
6649
+ "use strict";
6650
+ import_node_fs2 = require("node:fs");
6651
+ import_node_path7 = require("node:path");
6652
+ MAX_WORD_CHARS = 100;
6653
+ PAD_ID = 0;
6654
+ UNK_ID = 100;
6655
+ CLS_ID = 101;
6656
+ SEP_ID = 102;
6657
+ LocalTokenizer = class _LocalTokenizer {
6658
+ vocab;
6659
+ maxLength;
6660
+ constructor(vocabPath, maxLength = 512) {
6661
+ this.maxLength = maxLength;
6662
+ this.vocab = this.loadVocab(vocabPath);
6663
+ }
6664
+ static fromModelDir(modelDir, maxLength = 512) {
6665
+ return new _LocalTokenizer((0, import_node_path7.resolve)(modelDir, "vocab.txt"), maxLength);
6666
+ }
6667
+ loadVocab(path) {
6668
+ const vocab = /* @__PURE__ */ new Map();
6669
+ const lines = (0, import_node_fs2.readFileSync)(path, "utf-8").split("\n");
6670
+ for (let i = 0; i < lines.length; i++) {
6671
+ const token = lines[i]?.replace(/\r$/, "");
6672
+ if (token)
6673
+ vocab.set(token, i);
6674
+ }
6675
+ return vocab;
6676
+ }
6677
+ tokenize(text, options = {}) {
6678
+ const { addSpecialTokens = true, returnOffsetMapping = false, truncation = false, maxLength = this.maxLength } = options;
6679
+ const preTokens = pretokenize(text);
6680
+ const allIds = [];
6681
+ const allOffsets = [];
6682
+ for (const { word, positions } of preTokens) {
6683
+ const { ids, spans } = tokenizeSubwords(word, this.vocab, UNK_ID);
6684
+ for (let i = 0; i < ids.length; i++) {
6685
+ allIds.push(ids[i] ?? UNK_ID);
6686
+ const span = spans[i] ?? [0, 0];
6687
+ const subPositions = positions.slice(span[0], span[1]);
6688
+ if (subPositions.length > 0) {
6689
+ const first = subPositions[0] ?? 0;
6690
+ const last = subPositions[subPositions.length - 1] ?? 0;
6691
+ allOffsets.push([first, last + 1]);
6692
+ } else {
6693
+ allOffsets.push([positions[0] ?? 0, positions[0] ?? 0]);
6694
+ }
6695
+ }
6696
+ }
6697
+ const contentMax = maxLength - (addSpecialTokens ? 2 : 0);
6698
+ if (truncation && allIds.length > contentMax) {
6699
+ allIds.length = contentMax;
6700
+ allOffsets.length = contentMax;
6701
+ }
6702
+ if (addSpecialTokens) {
6703
+ allIds.unshift(CLS_ID);
6704
+ allIds.push(SEP_ID);
6705
+ allOffsets.unshift([0, 0]);
6706
+ allOffsets.push([0, 0]);
6707
+ }
6708
+ const result = {
6709
+ input_ids: allIds,
6710
+ attention_mask: new Array(allIds.length).fill(1),
6711
+ token_type_ids: new Array(allIds.length).fill(0)
6712
+ };
6713
+ if (returnOffsetMapping) {
6714
+ result.offset_mapping = allOffsets;
6715
+ }
6716
+ return result;
6717
+ }
6718
+ /**
6719
+ * Callable interface using snake_case option keys.
6720
+ * Supports: tokenizer(text, { truncation, max_length, padding })
6721
+ */
6722
+ call(text, options = {}) {
6723
+ const result = this.tokenize(text, {
6724
+ addSpecialTokens: options.add_special_tokens ?? true,
6725
+ returnOffsetMapping: options.return_offsets_mapping ?? false,
6726
+ truncation: options.truncation ?? false,
6727
+ maxLength: options.max_length,
6728
+ padding: options.padding ?? false
6729
+ });
6730
+ if (options.padding && result.input_ids.length < (options.max_length ?? this.maxLength)) {
6731
+ const padLen = (options.max_length ?? this.maxLength) - result.input_ids.length;
6732
+ result.input_ids.push(...new Array(padLen).fill(PAD_ID));
6733
+ result.attention_mask.push(...new Array(padLen).fill(0));
6734
+ result.token_type_ids.push(...new Array(padLen).fill(0));
6735
+ if (result.offset_mapping) {
6736
+ result.offset_mapping.push(...new Array(padLen).fill([0, 0]));
6737
+ }
6738
+ }
6739
+ return result;
6740
+ }
6741
+ };
6742
+ }
6743
+ });
6744
+
6745
+ // ../core/dist/clients/pi-deps-installer.js
6746
+ var pi_deps_installer_exports = {};
6747
+ __export(pi_deps_installer_exports, {
6748
+ ensurePiDeps: () => ensurePiDeps,
6749
+ isLocalInstallValid: () => isLocalInstallValid
6750
+ });
6751
+ function isLocalInstallValid(modelPath) {
6752
+ try {
6753
+ const req = (0, import_node_module.createRequire)((0, import_node_path8.resolve)(modelPath, "package.json"));
6754
+ const localPrefix = (0, import_node_fs3.realpathSync)((0, import_node_path8.resolve)(modelPath, "node_modules")) + import_node_path8.sep;
6755
+ for (const pkg of REQUIRED_PACKAGES) {
6756
+ const resolved = req.resolve(pkg);
6757
+ if (!resolved.startsWith(localPrefix))
6758
+ return false;
6759
+ }
6760
+ return true;
6761
+ } catch {
6762
+ return false;
6763
+ }
6764
+ }
6765
+ async function ensurePiDeps(modelPath, logger = nullLogger) {
6766
+ if (isLocalInstallValid(modelPath))
6767
+ return true;
6768
+ try {
6769
+ require.resolve("onnxruntime-node");
6770
+ return true;
6771
+ } catch {
6772
+ }
6773
+ logger.info("Installing PI runtime dependencies (first-time setup)...");
6774
+ try {
6775
+ const pkgJsonPath = (0, import_node_path8.resolve)(modelPath, "package.json");
6776
+ if (!(0, import_node_fs3.existsSync)(pkgJsonPath)) {
6777
+ (0, import_node_fs3.mkdirSync)(modelPath, { recursive: true });
6778
+ (0, import_node_fs3.writeFileSync)(pkgJsonPath, '{"private":true}');
6779
+ }
6780
+ await runNpm(modelPath, ["install", "--no-save", ...REQUIRED_PACKAGES], logger);
6781
+ const verified = isLocalInstallValid(modelPath);
6782
+ if (verified) {
6783
+ logger.info("PI runtime dependencies installed successfully");
6784
+ } else {
6785
+ logger.warn("PI dependency installation may be incomplete");
6786
+ }
6787
+ return verified;
6788
+ } catch (err) {
6789
+ logger.warn(`Failed to install PI dependencies (PI check will be skipped): ${err instanceof Error ? err.message : String(err)}`);
6790
+ return false;
6791
+ }
6792
+ }
6793
+ function runNpm(cwd, args, logger) {
6794
+ return new Promise((resolve9, reject) => {
6795
+ const isWindows = process.platform === "win32";
6796
+ const npmCmd = isWindows ? "npm.cmd" : "npm";
6797
+ const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
6798
+ if (error) {
6799
+ logger.warn(`npm install error: ${stderr || error.message}`);
6800
+ reject(error);
6801
+ } else {
6802
+ resolve9();
6803
+ }
6804
+ });
6805
+ child.unref();
6806
+ });
6807
+ }
6808
+ var import_node_child_process, import_node_fs3, import_node_module, import_node_path8, REQUIRED_PACKAGES;
6809
+ var init_pi_deps_installer = __esm({
6810
+ "../core/dist/clients/pi-deps-installer.js"() {
6811
+ "use strict";
6812
+ import_node_child_process = require("node:child_process");
6813
+ import_node_fs3 = require("node:fs");
6814
+ import_node_module = require("node:module");
6815
+ import_node_path8 = require("node:path");
6816
+ init_types2();
6817
+ REQUIRED_PACKAGES = ["onnxruntime-node"];
6818
+ }
6819
+ });
6820
+
6392
6821
  // ../../node_modules/.pnpm/yaml@2.8.2/node_modules/yaml/dist/nodes/identity.js
6393
6822
  var require_identity = __commonJS({
6394
6823
  "../../node_modules/.pnpm/yaml@2.8.2/node_modules/yaml/dist/nodes/identity.js"(exports2) {
@@ -10337,10 +10766,10 @@ var require_resolve_block_map = __commonJS({
10337
10766
  let offset = bm.offset;
10338
10767
  let commentEnd = null;
10339
10768
  for (const collItem of bm.items) {
10340
- const { start, key, sep: sep3, value } = collItem;
10769
+ const { start, key, sep: sep5, value } = collItem;
10341
10770
  const keyProps = resolveProps.resolveProps(start, {
10342
10771
  indicator: "explicit-key-ind",
10343
- next: key ?? sep3?.[0],
10772
+ next: key ?? sep5?.[0],
10344
10773
  offset,
10345
10774
  onError,
10346
10775
  parentIndent: bm.indent,
@@ -10354,7 +10783,7 @@ var require_resolve_block_map = __commonJS({
10354
10783
  else if ("indent" in key && key.indent !== bm.indent)
10355
10784
  onError(offset, "BAD_INDENT", startColMsg);
10356
10785
  }
10357
- if (!keyProps.anchor && !keyProps.tag && !sep3) {
10786
+ if (!keyProps.anchor && !keyProps.tag && !sep5) {
10358
10787
  commentEnd = keyProps.end;
10359
10788
  if (keyProps.comment) {
10360
10789
  if (map.comment)
@@ -10378,7 +10807,7 @@ var require_resolve_block_map = __commonJS({
10378
10807
  ctx.atKey = false;
10379
10808
  if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
10380
10809
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
10381
- const valueProps = resolveProps.resolveProps(sep3 ?? [], {
10810
+ const valueProps = resolveProps.resolveProps(sep5 ?? [], {
10382
10811
  indicator: "map-value-ind",
10383
10812
  next: value,
10384
10813
  offset: keyNode.range[2],
@@ -10394,7 +10823,7 @@ var require_resolve_block_map = __commonJS({
10394
10823
  if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
10395
10824
  onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
10396
10825
  }
10397
- const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep3, null, valueProps, onError);
10826
+ const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep5, null, valueProps, onError);
10398
10827
  if (ctx.schema.compat)
10399
10828
  utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
10400
10829
  offset = valueNode.range[2];
@@ -10485,7 +10914,7 @@ var require_resolve_end = __commonJS({
10485
10914
  let comment = "";
10486
10915
  if (end) {
10487
10916
  let hasSpace = false;
10488
- let sep3 = "";
10917
+ let sep5 = "";
10489
10918
  for (const token of end) {
10490
10919
  const { source, type } = token;
10491
10920
  switch (type) {
@@ -10499,13 +10928,13 @@ var require_resolve_end = __commonJS({
10499
10928
  if (!comment)
10500
10929
  comment = cb;
10501
10930
  else
10502
- comment += sep3 + cb;
10503
- sep3 = "";
10931
+ comment += sep5 + cb;
10932
+ sep5 = "";
10504
10933
  break;
10505
10934
  }
10506
10935
  case "newline":
10507
10936
  if (comment)
10508
- sep3 += source;
10937
+ sep5 += source;
10509
10938
  hasSpace = true;
10510
10939
  break;
10511
10940
  default:
@@ -10548,18 +10977,18 @@ var require_resolve_flow_collection = __commonJS({
10548
10977
  let offset = fc.offset + fc.start.source.length;
10549
10978
  for (let i = 0; i < fc.items.length; ++i) {
10550
10979
  const collItem = fc.items[i];
10551
- const { start, key, sep: sep3, value } = collItem;
10980
+ const { start, key, sep: sep5, value } = collItem;
10552
10981
  const props = resolveProps.resolveProps(start, {
10553
10982
  flow: fcName,
10554
10983
  indicator: "explicit-key-ind",
10555
- next: key ?? sep3?.[0],
10984
+ next: key ?? sep5?.[0],
10556
10985
  offset,
10557
10986
  onError,
10558
10987
  parentIndent: fc.indent,
10559
10988
  startOnNewline: false
10560
10989
  });
10561
10990
  if (!props.found) {
10562
- if (!props.anchor && !props.tag && !sep3 && !value) {
10991
+ if (!props.anchor && !props.tag && !sep5 && !value) {
10563
10992
  if (i === 0 && props.comma)
10564
10993
  onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
10565
10994
  else if (i < fc.items.length - 1)
@@ -10613,8 +11042,8 @@ var require_resolve_flow_collection = __commonJS({
10613
11042
  }
10614
11043
  }
10615
11044
  }
10616
- if (!isMap && !sep3 && !props.found) {
10617
- const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep3, null, props, onError);
11045
+ if (!isMap && !sep5 && !props.found) {
11046
+ const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep5, null, props, onError);
10618
11047
  coll.items.push(valueNode);
10619
11048
  offset = valueNode.range[2];
10620
11049
  if (isBlock(value))
@@ -10626,7 +11055,7 @@ var require_resolve_flow_collection = __commonJS({
10626
11055
  if (isBlock(key))
10627
11056
  onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
10628
11057
  ctx.atKey = false;
10629
- const valueProps = resolveProps.resolveProps(sep3 ?? [], {
11058
+ const valueProps = resolveProps.resolveProps(sep5 ?? [], {
10630
11059
  flow: fcName,
10631
11060
  indicator: "map-value-ind",
10632
11061
  next: value,
@@ -10637,8 +11066,8 @@ var require_resolve_flow_collection = __commonJS({
10637
11066
  });
10638
11067
  if (valueProps.found) {
10639
11068
  if (!isMap && !props.found && ctx.options.strict) {
10640
- if (sep3)
10641
- for (const st of sep3) {
11069
+ if (sep5)
11070
+ for (const st of sep5) {
10642
11071
  if (st === valueProps.found)
10643
11072
  break;
10644
11073
  if (st.type === "newline") {
@@ -10655,7 +11084,7 @@ var require_resolve_flow_collection = __commonJS({
10655
11084
  else
10656
11085
  onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
10657
11086
  }
10658
- const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep3, null, valueProps, onError) : null;
11087
+ const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep5, null, valueProps, onError) : null;
10659
11088
  if (valueNode) {
10660
11089
  if (isBlock(value))
10661
11090
  onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
@@ -10835,7 +11264,7 @@ var require_resolve_block_scalar = __commonJS({
10835
11264
  chompStart = i + 1;
10836
11265
  }
10837
11266
  let value = "";
10838
- let sep3 = "";
11267
+ let sep5 = "";
10839
11268
  let prevMoreIndented = false;
10840
11269
  for (let i = 0; i < contentStart; ++i)
10841
11270
  value += lines[i][0].slice(trimIndent) + "\n";
@@ -10852,24 +11281,24 @@ var require_resolve_block_scalar = __commonJS({
10852
11281
  indent = "";
10853
11282
  }
10854
11283
  if (type === Scalar.Scalar.BLOCK_LITERAL) {
10855
- value += sep3 + indent.slice(trimIndent) + content;
10856
- sep3 = "\n";
11284
+ value += sep5 + indent.slice(trimIndent) + content;
11285
+ sep5 = "\n";
10857
11286
  } else if (indent.length > trimIndent || content[0] === " ") {
10858
- if (sep3 === " ")
10859
- sep3 = "\n";
10860
- else if (!prevMoreIndented && sep3 === "\n")
10861
- sep3 = "\n\n";
10862
- value += sep3 + indent.slice(trimIndent) + content;
10863
- sep3 = "\n";
11287
+ if (sep5 === " ")
11288
+ sep5 = "\n";
11289
+ else if (!prevMoreIndented && sep5 === "\n")
11290
+ sep5 = "\n\n";
11291
+ value += sep5 + indent.slice(trimIndent) + content;
11292
+ sep5 = "\n";
10864
11293
  prevMoreIndented = true;
10865
11294
  } else if (content === "") {
10866
- if (sep3 === "\n")
11295
+ if (sep5 === "\n")
10867
11296
  value += "\n";
10868
11297
  else
10869
- sep3 = "\n";
11298
+ sep5 = "\n";
10870
11299
  } else {
10871
- value += sep3 + content;
10872
- sep3 = " ";
11300
+ value += sep5 + content;
11301
+ sep5 = " ";
10873
11302
  prevMoreIndented = false;
10874
11303
  }
10875
11304
  }
@@ -11051,25 +11480,25 @@ var require_resolve_flow_scalar = __commonJS({
11051
11480
  if (!match)
11052
11481
  return source;
11053
11482
  let res = match[1];
11054
- let sep3 = " ";
11483
+ let sep5 = " ";
11055
11484
  let pos = first.lastIndex;
11056
11485
  line.lastIndex = pos;
11057
11486
  while (match = line.exec(source)) {
11058
11487
  if (match[1] === "") {
11059
- if (sep3 === "\n")
11060
- res += sep3;
11488
+ if (sep5 === "\n")
11489
+ res += sep5;
11061
11490
  else
11062
- sep3 = "\n";
11491
+ sep5 = "\n";
11063
11492
  } else {
11064
- res += sep3 + match[1];
11065
- sep3 = " ";
11493
+ res += sep5 + match[1];
11494
+ sep5 = " ";
11066
11495
  }
11067
11496
  pos = line.lastIndex;
11068
11497
  }
11069
11498
  const last = /[ \t]*(.*)/sy;
11070
11499
  last.lastIndex = pos;
11071
11500
  match = last.exec(source);
11072
- return res + sep3 + (match?.[1] ?? "");
11501
+ return res + sep5 + (match?.[1] ?? "");
11073
11502
  }
11074
11503
  function doubleQuotedValue(source, onError) {
11075
11504
  let res = "";
@@ -11870,14 +12299,14 @@ var require_cst_stringify = __commonJS({
11870
12299
  }
11871
12300
  }
11872
12301
  }
11873
- function stringifyItem({ start, key, sep: sep3, value }) {
12302
+ function stringifyItem({ start, key, sep: sep5, value }) {
11874
12303
  let res = "";
11875
12304
  for (const st of start)
11876
12305
  res += st.source;
11877
12306
  if (key)
11878
12307
  res += stringifyToken(key);
11879
- if (sep3)
11880
- for (const st of sep3)
12308
+ if (sep5)
12309
+ for (const st of sep5)
11881
12310
  res += st.source;
11882
12311
  if (value)
11883
12312
  res += stringifyToken(value);
@@ -13026,18 +13455,18 @@ var require_parser = __commonJS({
13026
13455
  if (this.type === "map-value-ind") {
13027
13456
  const prev = getPrevProps(this.peek(2));
13028
13457
  const start = getFirstKeyStartProps(prev);
13029
- let sep3;
13458
+ let sep5;
13030
13459
  if (scalar.end) {
13031
- sep3 = scalar.end;
13032
- sep3.push(this.sourceToken);
13460
+ sep5 = scalar.end;
13461
+ sep5.push(this.sourceToken);
13033
13462
  delete scalar.end;
13034
13463
  } else
13035
- sep3 = [this.sourceToken];
13464
+ sep5 = [this.sourceToken];
13036
13465
  const map = {
13037
13466
  type: "block-map",
13038
13467
  offset: scalar.offset,
13039
13468
  indent: scalar.indent,
13040
- items: [{ start, key: scalar, sep: sep3 }]
13469
+ items: [{ start, key: scalar, sep: sep5 }]
13041
13470
  };
13042
13471
  this.onKeyLine = true;
13043
13472
  this.stack[this.stack.length - 1] = map;
@@ -13190,15 +13619,15 @@ var require_parser = __commonJS({
13190
13619
  } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
13191
13620
  const start2 = getFirstKeyStartProps(it.start);
13192
13621
  const key = it.key;
13193
- const sep3 = it.sep;
13194
- sep3.push(this.sourceToken);
13622
+ const sep5 = it.sep;
13623
+ sep5.push(this.sourceToken);
13195
13624
  delete it.key;
13196
13625
  delete it.sep;
13197
13626
  this.stack.push({
13198
13627
  type: "block-map",
13199
13628
  offset: this.offset,
13200
13629
  indent: this.indent,
13201
- items: [{ start: start2, key, sep: sep3 }]
13630
+ items: [{ start: start2, key, sep: sep5 }]
13202
13631
  });
13203
13632
  } else if (start.length > 0) {
13204
13633
  it.sep = it.sep.concat(start, this.sourceToken);
@@ -13392,13 +13821,13 @@ var require_parser = __commonJS({
13392
13821
  const prev = getPrevProps(parent);
13393
13822
  const start = getFirstKeyStartProps(prev);
13394
13823
  fixFlowSeqItems(fc);
13395
- const sep3 = fc.end.splice(1, fc.end.length);
13396
- sep3.push(this.sourceToken);
13824
+ const sep5 = fc.end.splice(1, fc.end.length);
13825
+ sep5.push(this.sourceToken);
13397
13826
  const map = {
13398
13827
  type: "block-map",
13399
13828
  offset: fc.offset,
13400
13829
  indent: fc.indent,
13401
- items: [{ start, key: fc, sep: sep3 }]
13830
+ items: [{ start, key: fc, sep: sep5 }]
13402
13831
  };
13403
13832
  this.onKeyLine = true;
13404
13833
  this.stack[this.stack.length - 1] = map;
@@ -13731,8 +14160,8 @@ function parseEntries(raw) {
13731
14160
  }
13732
14161
  return entries;
13733
14162
  }
13734
- async function loadAllowlist(config, logger = nullLogger) {
13735
- const path = resolvePath(config.path);
14163
+ async function loadAllowlist(config2, logger = nullLogger) {
14164
+ const path = resolvePath(config2.path);
13736
14165
  let raw;
13737
14166
  try {
13738
14167
  raw = await getFileContent(path);
@@ -13889,6 +14318,7 @@ var import_node_path4 = require("node:path");
13889
14318
  init_config();
13890
14319
  init_file_utils();
13891
14320
  var MAX_SUMMARY_LEN = 200;
14321
+ var AUDIT_LOG_SCHEMA_VERSION = 1;
13892
14322
  function toolInputSummary(toolName, toolInput) {
13893
14323
  if (toolName === "Bash") {
13894
14324
  return String(toolInput.command ?? "").slice(0, MAX_SUMMARY_LEN);
@@ -13928,43 +14358,50 @@ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
13928
14358
  } catch {
13929
14359
  }
13930
14360
  }
13931
- async function appendEntry(config, entry) {
13932
- const path = resolvePath(config.path);
14361
+ async function appendEntry(config2, entry) {
14362
+ const path = resolvePath(config2.path);
13933
14363
  await (0, import_promises.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
13934
- await rotateIfNeeded(path, config.max_bytes, config.max_files);
13935
- await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
14364
+ await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
14365
+ const stamped = { ...entry, schema_version: AUDIT_LOG_SCHEMA_VERSION };
14366
+ await (0, import_promises.appendFile)(path, `${JSON.stringify(stamped)}
13936
14367
  `);
13937
14368
  }
13938
- async function logVerdict(config, sessionId, toolName, toolInput, verdict, userOverride = false, conversationId, agentRuntime, hookType, signals, eventId) {
13939
- if (!config.enabled)
14369
+ async function logVerdict(config2, input) {
14370
+ if (!config2.enabled)
13940
14371
  return;
13941
- if (verdict.decision === "allow" && !config.log_clean && !userOverride)
14372
+ const userOverride = input.userOverride ?? false;
14373
+ const hasSignals = input.signals != null && Object.keys(input.signals).length > 0;
14374
+ if (input.verdict.decision === "allow" && !config2.log_clean && !userOverride && !hasSignals)
13942
14375
  return;
13943
14376
  const entry = {
13944
14377
  type: "runtime_verdict",
13945
- entry_id: eventId ?? (0, import_node_crypto4.randomUUID)(),
14378
+ entry_id: input.eventId ?? (0, import_node_crypto4.randomUUID)(),
13946
14379
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
13947
- session_id: sessionId,
13948
- conversation_id: conversationId ?? sessionId,
13949
- agent_runtime: agentRuntime,
13950
- hook_type: hookType,
13951
- tool_name: toolName,
13952
- tool_input_summary: toolInputSummary(toolName, toolInput),
13953
- artifacts: verdict.artifacts,
13954
- verdict: verdict.decision,
13955
- severity: verdict.severity,
13956
- reasons: verdict.reasons,
13957
- source: verdict.source,
14380
+ session_id: input.sessionId,
14381
+ conversation_id: input.conversationId ?? input.sessionId,
14382
+ agent_runtime: input.agentRuntime,
14383
+ hook_type: input.hookType,
14384
+ tool_name: input.toolName,
14385
+ tool_input_summary: toolInputSummary(input.toolName, input.toolInput),
14386
+ artifacts: input.verdict.artifacts,
14387
+ verdict: input.verdict.decision,
14388
+ severity: input.verdict.severity,
14389
+ reasons: input.verdict.reasons,
14390
+ source: input.verdict.source,
13958
14391
  user_override: userOverride,
13959
- signals
14392
+ signals: input.signals,
14393
+ tool_use_id: input.toolUseId
13960
14394
  };
14395
+ if (input.content !== void 0) {
14396
+ entry.content = input.content;
14397
+ }
13961
14398
  try {
13962
- await appendEntry(config, entry);
14399
+ await appendEntry(config2, entry);
13963
14400
  } catch {
13964
14401
  }
13965
14402
  }
13966
- async function logPluginScan(config, pluginKey, pluginVersion, findings) {
13967
- if (!config.enabled)
14403
+ async function logPluginScan(config2, pluginKey, pluginVersion, findings) {
14404
+ if (!config2.enabled)
13968
14405
  return;
13969
14406
  const entry = {
13970
14407
  type: "plugin_scan",
@@ -13975,48 +14412,25 @@ async function logPluginScan(config, pluginKey, pluginVersion, findings) {
13975
14412
  findings
13976
14413
  };
13977
14414
  try {
13978
- await appendEntry(config, entry);
14415
+ await appendEntry(config2, entry);
13979
14416
  } catch {
13980
14417
  }
13981
14418
  }
13982
14419
 
13983
- // ../core/dist/branding.js
13984
- var import_node_path5 = require("node:path");
13985
- init_config();
13986
- init_file_utils();
13987
- init_types2();
13988
- var defaultBranding = BrandingSchema.parse({});
13989
- function defaultBrandingPath() {
13990
- return (0, import_node_path5.join)(resolvePath(SAGE_DIR), "branding.json");
13991
- }
13992
- function loadBrandingSync(logger = nullLogger, brandingPath) {
13993
- const path = brandingPath ?? defaultBrandingPath();
13994
- let raw;
13995
- try {
13996
- raw = getFileContentSync(path);
13997
- } catch {
13998
- return defaultBranding;
13999
- }
14000
- return parseBranding(raw, path, logger);
14001
- }
14002
- function parseBranding(raw, path, logger) {
14003
- let data;
14004
- try {
14005
- data = JSON.parse(raw);
14006
- } catch (e) {
14007
- logger.warn(`Failed to parse branding from ${path}`, { error: String(e) });
14008
- return defaultBranding;
14009
- }
14010
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
14011
- logger.warn(`Branding file ${path} does not contain a JSON object`);
14420
+ // ../core/dist/brands.js
14421
+ var defaultBranding = { name: "Sage", short_name: "Sage" };
14422
+ var BRANDS = {
14423
+ norton: { name: "Norton AI Agent Protection", short_name: "Norton" }
14424
+ };
14425
+ function resolveBranding(brandKey, logger) {
14426
+ if (!brandKey)
14012
14427
  return defaultBranding;
14013
- }
14014
- try {
14015
- return BrandingSchema.parse(data);
14016
- } catch (e) {
14017
- logger.warn("Branding validation failed, using defaults", { error: String(e) });
14428
+ const entry = BRANDS[brandKey];
14429
+ if (!entry) {
14430
+ logger?.warn(`Unknown brand_key "${brandKey}" in config \u2014 using default branding`);
14018
14431
  return defaultBranding;
14019
14432
  }
14433
+ return { ...entry, brand_key: brandKey };
14020
14434
  }
14021
14435
 
14022
14436
  // ../core/dist/cache.js
@@ -14032,10 +14446,10 @@ var VerdictCache = class {
14032
14446
  config;
14033
14447
  logger;
14034
14448
  version;
14035
- constructor(config, logger = nullLogger, version) {
14036
- this.config = config;
14449
+ constructor(config2, logger = nullLogger, version) {
14450
+ this.config = config2;
14037
14451
  this.logger = logger;
14038
- this.path = resolvePath(config.path);
14452
+ this.path = resolvePath(config2.path);
14039
14453
  this.version = version;
14040
14454
  }
14041
14455
  async load() {
@@ -14064,7 +14478,8 @@ var VerdictCache = class {
14064
14478
  verdict: entry.verdict,
14065
14479
  severity: entry.severity,
14066
14480
  reasons: entry.reasons,
14067
- source: entry.source
14481
+ source: entry.source,
14482
+ ...entry.urlSignalLabels !== void 0 ? { urlSignalLabels: entry.urlSignalLabels } : {}
14068
14483
  };
14069
14484
  }
14070
14485
  putUrl(url, verdict, isMalicious) {
@@ -14097,9 +14512,10 @@ var VerdictCache = class {
14097
14512
  putCommand(commandHash, verdict) {
14098
14513
  if (!this.config.enabled)
14099
14514
  return;
14515
+ const { urlSignalLabels: _ignoredUrlLabels, ...rest } = verdict;
14100
14516
  const now = /* @__PURE__ */ new Date();
14101
14517
  this.store.commands[commandHash] = {
14102
- ...verdict,
14518
+ ...rest,
14103
14519
  checkedAt: now.toISOString(),
14104
14520
  expiresAt: FAR_FUTURE,
14105
14521
  sageVersion: this.version
@@ -14121,11 +14537,12 @@ var VerdictCache = class {
14121
14537
  putPackage(key, verdict, packageAgeDays) {
14122
14538
  if (!this.config.enabled)
14123
14539
  return;
14540
+ const { urlSignalLabels: _ignoredUrlLabels, ...rest } = verdict;
14124
14541
  const ttlSeconds = this.computePackageTtl(verdict.verdict, packageAgeDays);
14125
14542
  const now = /* @__PURE__ */ new Date();
14126
14543
  const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
14127
14544
  this.store.packages[key] = {
14128
- ...verdict,
14545
+ ...rest,
14129
14546
  checkedAt: now.toISOString(),
14130
14547
  expiresAt: expiresAt.toISOString(),
14131
14548
  sageVersion: this.version
@@ -14211,8 +14628,12 @@ function interpretAmsiResult(amsiResult, content, contentName) {
14211
14628
  var KoffiAmsiBackend = class {
14212
14629
  logger;
14213
14630
  context = null;
14214
- session = null;
14215
14631
  available = false;
14632
+ /** Koffi library handle — must be closed to let the event loop drain. */
14633
+ /* biome-ignore lint/suspicious/noExplicitAny: koffi Library type is not exported */
14634
+ lib = null;
14635
+ /* biome-ignore lint/suspicious/noExplicitAny: koffi FFI functions have dynamic signatures */
14636
+ fnOpenSession = null;
14216
14637
  /* biome-ignore lint/suspicious/noExplicitAny: koffi FFI functions have dynamic signatures */
14217
14638
  fnScanBuffer = null;
14218
14639
  /* biome-ignore lint/suspicious/noExplicitAny: koffi FFI functions have dynamic signatures */
@@ -14230,10 +14651,11 @@ var KoffiAmsiBackend = class {
14230
14651
  const koffiModule = await import("koffi");
14231
14652
  const koffi = koffiModule.default ?? koffiModule;
14232
14653
  const lib = koffi.load("amsi.dll");
14654
+ this.lib = lib;
14233
14655
  const _HAMSICONTEXT = koffi.pointer("HAMSICONTEXT", koffi.opaque());
14234
14656
  const _HAMSISESSION = koffi.pointer("HAMSISESSION", koffi.opaque());
14235
14657
  const AmsiInitialize = lib.func("int32 __stdcall AmsiInitialize(str16 appName, _Out_ HAMSICONTEXT *ctx)");
14236
- const AmsiOpenSession = lib.func("int32 __stdcall AmsiOpenSession(HAMSICONTEXT ctx, _Out_ HAMSISESSION *session)");
14658
+ this.fnOpenSession = lib.func("int32 __stdcall AmsiOpenSession(HAMSICONTEXT ctx, _Out_ HAMSISESSION *session)");
14237
14659
  this.fnScanBuffer = lib.func("int32 __stdcall AmsiScanBuffer(HAMSICONTEXT ctx, void *buf, uint32 len, str16 contentName, HAMSISESSION session, _Out_ int32 *result)");
14238
14660
  this.fnCloseSession = lib.func("void __stdcall AmsiCloseSession(HAMSICONTEXT ctx, HAMSISESSION session)");
14239
14661
  this.fnUninitialize = lib.func("void __stdcall AmsiUninitialize(HAMSICONTEXT ctx)");
@@ -14241,39 +14663,44 @@ var KoffiAmsiBackend = class {
14241
14663
  const hr = AmsiInitialize("Sage", ctxOut);
14242
14664
  if (hr !== 0) {
14243
14665
  this.logger.warn("AMSI: koffi AmsiInitialize failed", { hr });
14666
+ this.close();
14244
14667
  return;
14245
14668
  }
14246
14669
  this.context = ctxOut[0];
14247
14670
  const sessOut = [null];
14248
- const hr2 = AmsiOpenSession(this.context, sessOut);
14671
+ const hr2 = this.fnOpenSession(this.context, sessOut);
14249
14672
  if (hr2 !== 0) {
14250
14673
  this.logger.warn("AMSI: koffi AmsiOpenSession failed", { hr: hr2 });
14251
- try {
14252
- if (this.fnUninitialize) {
14253
- this.fnUninitialize(this.context);
14254
- }
14255
- } catch {
14256
- }
14257
- this.context = null;
14674
+ this.close();
14258
14675
  return;
14259
14676
  }
14260
- this.session = sessOut[0];
14677
+ try {
14678
+ this.fnCloseSession(this.context, sessOut[0]);
14679
+ } catch {
14680
+ }
14261
14681
  this.available = true;
14262
14682
  this.logger.debug("AMSI: koffi backend initialized");
14263
14683
  } catch (e) {
14264
14684
  this.logger.debug("AMSI: koffi backend init failed", { error: String(e) });
14265
- this.available = false;
14685
+ this.close();
14266
14686
  }
14267
14687
  }
14268
14688
  async scanString(content, contentName) {
14269
- if (!this.available || !this.fnScanBuffer || !this.context || !this.session) {
14689
+ if (!this.available || !this.fnOpenSession || !this.fnScanBuffer || !this.context) {
14270
14690
  return null;
14271
14691
  }
14692
+ const sessOut = [null];
14693
+ const hrOpen = this.fnOpenSession(this.context, sessOut);
14694
+ if (hrOpen !== 0) {
14695
+ this.logger.warn("AMSI: koffi AmsiOpenSession failed in scan", { hr: hrOpen, contentName });
14696
+ return null;
14697
+ }
14698
+ const session = sessOut[0];
14272
14699
  try {
14273
14700
  const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
14274
14701
  const buf = Buffer.from(truncated, "utf-8");
14275
14702
  const resultOut = [0];
14276
- const hr = this.fnScanBuffer(this.context, buf, buf.length, contentName, this.session, resultOut);
14703
+ const hr = this.fnScanBuffer(this.context, buf, buf.length, contentName, session, resultOut);
14277
14704
  if (hr !== 0) {
14278
14705
  this.logger.warn("AMSI: koffi AmsiScanBuffer failed", { hr, contentName });
14279
14706
  return null;
@@ -14284,24 +14711,33 @@ var KoffiAmsiBackend = class {
14284
14711
  } catch (e) {
14285
14712
  this.logger.warn("AMSI: koffi scanBuffer failed", { error: String(e), contentName });
14286
14713
  return null;
14714
+ } finally {
14715
+ try {
14716
+ if (this.fnCloseSession && this.context) {
14717
+ this.fnCloseSession(this.context, session);
14718
+ }
14719
+ } catch {
14720
+ }
14287
14721
  }
14288
14722
  }
14289
14723
  close() {
14290
- try {
14291
- if (this.session && this.context && this.fnCloseSession) {
14292
- this.fnCloseSession(this.context, this.session);
14293
- }
14294
- } catch {
14295
- }
14296
14724
  try {
14297
14725
  if (this.context && this.fnUninitialize) {
14298
14726
  this.fnUninitialize(this.context);
14299
14727
  }
14300
14728
  } catch {
14301
14729
  }
14302
- this.session = null;
14730
+ this.fnOpenSession = null;
14731
+ this.fnScanBuffer = null;
14732
+ this.fnCloseSession = null;
14733
+ this.fnUninitialize = null;
14303
14734
  this.context = null;
14304
14735
  this.available = false;
14736
+ try {
14737
+ this.lib?.close();
14738
+ } catch {
14739
+ }
14740
+ this.lib = null;
14305
14741
  }
14306
14742
  };
14307
14743
  var AMSI_CSHARP_TYPE = `
@@ -14328,34 +14764,45 @@ public class SageAmsi {
14328
14764
  static extern void AmsiUninitialize(IntPtr ctx);
14329
14765
 
14330
14766
  private static IntPtr _ctx;
14331
- private static IntPtr _session;
14332
14767
  private static bool _initialized;
14333
14768
 
14334
14769
  public static bool Init() {
14335
14770
  int hr = AmsiInitialize("Sage", out _ctx);
14336
14771
  if (hr != 0) return false;
14337
- hr = AmsiOpenSession(_ctx, out _session);
14772
+ // Verify we can open a session, then close it immediately.
14773
+ // Actual sessions are opened per-scan to avoid cross-file tainting.
14774
+ IntPtr sess;
14775
+ hr = AmsiOpenSession(_ctx, out sess);
14338
14776
  if (hr != 0) {
14339
14777
  AmsiUninitialize(_ctx);
14340
14778
  return false;
14341
14779
  }
14780
+ AmsiCloseSession(_ctx, sess);
14342
14781
  _initialized = true;
14343
14782
  return true;
14344
14783
  }
14345
14784
 
14346
14785
  public static int Scan(string content, string contentName) {
14347
14786
  if (!_initialized) return -1;
14348
- byte[] bytes = Encoding.UTF8.GetBytes(content);
14349
- int result;
14350
- int hr = AmsiScanBuffer(_ctx, bytes, (uint)bytes.Length,
14351
- contentName, _session, out result);
14787
+ // Open a fresh session per scan so a detection in one file
14788
+ // does not taint subsequent scans (sessions are correlation scopes).
14789
+ IntPtr session;
14790
+ int hr = AmsiOpenSession(_ctx, out session);
14352
14791
  if (hr != 0) return -1;
14353
- return result;
14792
+ try {
14793
+ byte[] bytes = Encoding.UTF8.GetBytes(content);
14794
+ int result;
14795
+ hr = AmsiScanBuffer(_ctx, bytes, (uint)bytes.Length,
14796
+ contentName, session, out result);
14797
+ if (hr != 0) return -1;
14798
+ return result;
14799
+ } finally {
14800
+ AmsiCloseSession(_ctx, session);
14801
+ }
14354
14802
  }
14355
14803
 
14356
14804
  public static void Shutdown() {
14357
14805
  if (!_initialized) return;
14358
- AmsiCloseSession(_ctx, _session);
14359
14806
  AmsiUninitialize(_ctx);
14360
14807
  _initialized = false;
14361
14808
  }
@@ -14435,17 +14882,24 @@ var PersistentPowershellAmsiBackend = class {
14435
14882
  return this.available;
14436
14883
  }
14437
14884
  waitForLine(timeout) {
14438
- return new Promise((resolve3, reject) => {
14885
+ return new Promise((resolve9, reject) => {
14439
14886
  const timer = setTimeout(() => {
14440
14887
  this.pendingResponse = null;
14441
14888
  reject(new Error("timeout"));
14442
14889
  }, timeout);
14443
- this.pendingResponse = { resolve: resolve3, reject, timer };
14890
+ this.pendingResponse = { resolve: resolve9, reject, timer };
14444
14891
  });
14445
14892
  }
14446
14893
  async init() {
14447
14894
  try {
14448
- this.process = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_PERSISTENT_SCRIPT], {
14895
+ this.process = spawn("powershell.exe", [
14896
+ "-NoProfile",
14897
+ "-NonInteractive",
14898
+ "-ExecutionPolicy",
14899
+ "Bypass",
14900
+ "-Command",
14901
+ PS_PERSISTENT_SCRIPT
14902
+ ], {
14449
14903
  stdio: ["pipe", "pipe", "pipe"],
14450
14904
  windowsHide: true
14451
14905
  });
@@ -14475,10 +14929,10 @@ var PersistentPowershellAmsiBackend = class {
14475
14929
  const line = this.stdoutBuffer.slice(0, idx).trim();
14476
14930
  this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
14477
14931
  if (this.pendingResponse) {
14478
- const { resolve: resolve3, timer } = this.pendingResponse;
14932
+ const { resolve: resolve9, timer } = this.pendingResponse;
14479
14933
  this.pendingResponse = null;
14480
14934
  clearTimeout(timer);
14481
- resolve3(line);
14935
+ resolve9(line);
14482
14936
  }
14483
14937
  idx = this.stdoutBuffer.indexOf("\n");
14484
14938
  }
@@ -14567,7 +15021,7 @@ var WslPowershellAmsiBackend = class {
14567
15021
  if (!this.available)
14568
15022
  return null;
14569
15023
  const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
14570
- return new Promise((resolve3) => {
15024
+ return new Promise((resolve9) => {
14571
15025
  try {
14572
15026
  const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
14573
15027
  let stdout = "";
@@ -14577,7 +15031,7 @@ var WslPowershellAmsiBackend = class {
14577
15031
  ps.kill();
14578
15032
  } catch {
14579
15033
  }
14580
- resolve3(null);
15034
+ resolve9(null);
14581
15035
  }, PS_TIMEOUT);
14582
15036
  ps.stdout?.on("data", (chunk) => {
14583
15037
  stdout += chunk.toString();
@@ -14590,7 +15044,7 @@ var WslPowershellAmsiBackend = class {
14590
15044
  ps.on("error", (err) => {
14591
15045
  clearTimeout(timer);
14592
15046
  this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
14593
- resolve3(null);
15047
+ resolve9(null);
14594
15048
  });
14595
15049
  ps.on("exit", () => {
14596
15050
  clearTimeout(timer);
@@ -14600,11 +15054,11 @@ var WslPowershellAmsiBackend = class {
14600
15054
  stdout: stdout.slice(0, 100),
14601
15055
  contentName
14602
15056
  });
14603
- resolve3(null);
15057
+ resolve9(null);
14604
15058
  return;
14605
15059
  }
14606
15060
  this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
14607
- resolve3(interpretAmsiResult(amsiResult, content, contentName));
15061
+ resolve9(interpretAmsiResult(amsiResult, content, contentName));
14608
15062
  });
14609
15063
  const req = JSON.stringify({ content: truncated, contentName });
14610
15064
  ps.stdin?.write(`${req}
@@ -14612,7 +15066,7 @@ var WslPowershellAmsiBackend = class {
14612
15066
  ps.stdin?.end();
14613
15067
  } catch (e) {
14614
15068
  this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
14615
- resolve3(null);
15069
+ resolve9(null);
14616
15070
  }
14617
15071
  });
14618
15072
  }
@@ -14651,8 +15105,9 @@ var AmsiClient = class {
14651
15105
  }
14652
15106
  this.logger.debug("AMSI: no backend available");
14653
15107
  }
14654
- async scanString(content, contentName) {
14655
- return await this.backend?.scanString(content, contentName) ?? null;
15108
+ async scanString(scanType, contentName, content) {
15109
+ const formattedName = `[Sage:${scanType}]:${contentName}`;
15110
+ return await this.backend?.scanString(content, formattedName) ?? null;
14656
15111
  }
14657
15112
  close() {
14658
15113
  this.backend?.close();
@@ -14660,19 +15115,106 @@ var AmsiClient = class {
14660
15115
  }
14661
15116
  };
14662
15117
 
15118
+ // ../core/dist/clients/content-fetch.js
15119
+ init_types2();
15120
+ var DEFAULT_TIMEOUT_MS = 4e3;
15121
+ var DEFAULT_MAX_CONTENT_LENGTH = 16384;
15122
+ var BINARY_SNIFF_LENGTH = 512;
15123
+ var TEXTUAL_APPLICATION_TYPES = /* @__PURE__ */ new Set([
15124
+ "application/json",
15125
+ "application/xml",
15126
+ "application/xhtml+xml",
15127
+ "application/javascript",
15128
+ "application/x-yaml",
15129
+ "application/yaml",
15130
+ "application/rss+xml",
15131
+ "application/atom+xml",
15132
+ "application/ld+json"
15133
+ ]);
15134
+ var ContentFetchClient = class _ContentFetchClient {
15135
+ timeoutMs;
15136
+ maxContentLength;
15137
+ logger;
15138
+ constructor(timeoutMs = DEFAULT_TIMEOUT_MS, maxContentLength = DEFAULT_MAX_CONTENT_LENGTH, logger = nullLogger) {
15139
+ this.timeoutMs = timeoutMs;
15140
+ this.maxContentLength = maxContentLength;
15141
+ this.logger = logger;
15142
+ }
15143
+ async fetchTextContent(url) {
15144
+ try {
15145
+ const response = await fetch(url, {
15146
+ signal: AbortSignal.timeout(this.timeoutMs),
15147
+ redirect: "follow",
15148
+ headers: { "User-Agent": "sage" }
15149
+ });
15150
+ if (!response.ok) {
15151
+ this.logger.debug("Content fetch failed", { url, status: String(response.status) });
15152
+ return null;
15153
+ }
15154
+ const contentType = response.headers.get("content-type");
15155
+ if (!_ContentFetchClient.isTextualContentType(contentType)) {
15156
+ this.logger.debug("Skipping non-textual content type", {
15157
+ url,
15158
+ contentType: contentType ?? "unknown"
15159
+ });
15160
+ return null;
15161
+ }
15162
+ const reader = response.body?.getReader();
15163
+ if (!reader)
15164
+ return null;
15165
+ const decoder = new TextDecoder();
15166
+ let text = "";
15167
+ try {
15168
+ while (text.length < this.maxContentLength) {
15169
+ const { done, value } = await reader.read();
15170
+ if (done)
15171
+ break;
15172
+ text += decoder.decode(value, { stream: true });
15173
+ }
15174
+ text += decoder.decode();
15175
+ } finally {
15176
+ reader.cancel();
15177
+ }
15178
+ if (text.length > this.maxContentLength) {
15179
+ text = text.slice(0, this.maxContentLength);
15180
+ }
15181
+ if (_ContentFetchClient.hasBinaryContent(text)) {
15182
+ this.logger.debug("Skipping binary content", { url });
15183
+ return null;
15184
+ }
15185
+ return { content: text, contentType };
15186
+ } catch (error) {
15187
+ this.logger.debug("Content fetch error", { url, error: String(error) });
15188
+ return null;
15189
+ }
15190
+ }
15191
+ static isTextualContentType(contentType) {
15192
+ if (contentType == null)
15193
+ return true;
15194
+ const mime = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
15195
+ if (mime.startsWith("text/"))
15196
+ return true;
15197
+ return TEXTUAL_APPLICATION_TYPES.has(mime);
15198
+ }
15199
+ static hasBinaryContent(text) {
15200
+ const sniff = text.slice(0, BINARY_SNIFF_LENGTH);
15201
+ return sniff.includes("\0");
15202
+ }
15203
+ };
15204
+
14663
15205
  // ../core/dist/clients/file-check.js
14664
15206
  init_types2();
14665
15207
 
14666
15208
  // ../core/dist/version.js
14667
- var import_node_path6 = require("node:path");
15209
+ var import_node_path5 = require("node:path");
14668
15210
  var import_node_url = require("node:url");
14669
15211
  init_file_utils();
14670
15212
  var import_meta = {};
14671
15213
  function resolveVersion() {
14672
15214
  if (true)
14673
- return "0.8.0";
15215
+ return "0.9.0";
14674
15216
  try {
14675
- const pkgPath = (0, import_node_path6.join)((0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
15217
+ const pkgPath = (0, import_node_path5.join)((0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
14676
15218
  const pkg = JSON.parse(getFileContentSync(pkgPath));
14677
15219
  if (typeof pkg.version === "string")
14678
15220
  return pkg.version;
@@ -14701,9 +15243,9 @@ var FileCheckClient = class {
14701
15243
  endpoint;
14702
15244
  timeoutMs;
14703
15245
  logger;
14704
- constructor(config, logger = nullLogger) {
14705
- this.endpoint = config?.endpoint ?? this.resolveEndpoint();
14706
- this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT) * 1e3;
15246
+ constructor(config2, logger = nullLogger) {
15247
+ this.endpoint = config2?.endpoint ?? this.resolveEndpoint();
15248
+ this.timeoutMs = (config2?.timeout_seconds ?? DEFAULT_TIMEOUT) * 1e3;
14707
15249
  this.logger = logger;
14708
15250
  }
14709
15251
  buildPath() {
@@ -14815,21 +15357,210 @@ var FileCheckClient = class {
14815
15357
  }
14816
15358
  };
14817
15359
 
14818
- // ../core/dist/clients/package-registry.js
14819
- var import_semver = __toESM(require_semver2(), 1);
15360
+ // ../core/dist/model-storage.js
15361
+ var import_node_fs = require("node:fs");
15362
+ var import_node_path6 = require("node:path");
15363
+ init_config();
15364
+ var MODEL_SCHEMA_VERSION = "v1";
15365
+ var REQUIRED_MODELS_BY_SCHEMA = {
15366
+ v1: ["pi-model"]
15367
+ };
15368
+ var REQUIRED_MODEL_FILES = {
15369
+ "pi-model": [
15370
+ "model_int8.onnx",
15371
+ "tokenizer.json",
15372
+ "tokenizer_config.json",
15373
+ "special_tokens_map.json",
15374
+ "vocab.txt",
15375
+ "config.json"
15376
+ ]
15377
+ };
15378
+ function getModelStorageRoot(sageDir = resolvePath("~/.sage")) {
15379
+ return (0, import_node_path6.join)(resolvePath(sageDir), "models");
15380
+ }
15381
+ function getModelDir(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
15382
+ return (0, import_node_path6.join)(getModelStorageRoot(sageDir), schema, modelName);
15383
+ }
15384
+ function isModelPresent(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
15385
+ const dir = getModelDir(modelName, schema, sageDir);
15386
+ const files = REQUIRED_MODEL_FILES[modelName];
15387
+ if (!files || files.length === 0)
15388
+ return false;
15389
+ return files.every((f) => (0, import_node_fs.existsSync)((0, import_node_path6.resolve)(dir, f)));
15390
+ }
15391
+ function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
15392
+ const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
15393
+ return required.filter((name) => !isModelPresent(name, schema, sageDir));
15394
+ }
15395
+ function anyRequiredModelMissing(schema = MODEL_SCHEMA_VERSION, sageDir) {
15396
+ return missingRequiredModels(schema, sageDir).length > 0;
15397
+ }
15398
+
15399
+ // ../core/dist/clients/model-downloader.js
14820
15400
  init_types2();
14821
- var NPM_REGISTRY = "https://registry.npmjs.org";
14822
- var PYPI_REGISTRY = "https://pypi.org/pypi";
14823
- var DEFAULT_TIMEOUT2 = 5;
14824
- var NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
14825
- var PYPI_NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;
14826
- var RegistryClient = class {
14827
- timeoutMs;
14828
- logger;
14829
- constructor(config, logger = nullLogger) {
14830
- this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT2) * 1e3;
14831
- this.logger = logger;
14832
- }
15401
+ var STALE_LOCK_MS = 60 * 60 * 1e3;
15402
+
15403
+ // ../core/dist/sage-proxy.js
15404
+ function mapSageProxyOs(platform) {
15405
+ switch (platform) {
15406
+ case "win32":
15407
+ return "WINDOWS";
15408
+ case "darwin":
15409
+ return "MACOS";
15410
+ case "linux":
15411
+ return "LINUX";
15412
+ default:
15413
+ return platform;
15414
+ }
15415
+ }
15416
+ function mapSageProxyArchitecture(arch) {
15417
+ return arch.toUpperCase();
15418
+ }
15419
+ function buildSageProxyEnvelope(args) {
15420
+ return {
15421
+ identity: { uuid: args.iid },
15422
+ product: { version_app: args.versionApp },
15423
+ platform: {
15424
+ os: args.platformOs ?? mapSageProxyOs(process.platform),
15425
+ architecture: args.platformArchitecture ?? mapSageProxyArchitecture(process.arch)
15426
+ },
15427
+ agent: {
15428
+ agent_runtime: args.agentRuntime,
15429
+ agent_runtime_version: args.agentRuntimeVersion
15430
+ }
15431
+ };
15432
+ }
15433
+
15434
+ // ../core/dist/clients/model-manifest.js
15435
+ init_types2();
15436
+
15437
+ // ../core/dist/clients/url-check.js
15438
+ init_types2();
15439
+ var DEFAULT_TIMEOUT2 = 5;
15440
+ var MAX_URLS_PER_REQUEST = 50;
15441
+ var SERVICE_NAME2 = "sage";
15442
+ function getProviderTld2() {
15443
+ return "com";
15444
+ }
15445
+ var REQUEST_HEADERS2 = [
15446
+ { name: "Accept", value: "application/json" },
15447
+ { name: "Content-Type", value: "application/json" },
15448
+ { name: "User-Agent", value: SERVICE_NAME2 }
15449
+ ];
15450
+ function getProviderName2() {
15451
+ return "avast";
15452
+ }
15453
+ function getSubdomain() {
15454
+ return "svc";
15455
+ }
15456
+ function buildDomain() {
15457
+ return [getSubdomain(), getProviderName2(), getProviderTld2()].join(".");
15458
+ }
15459
+ function resolveEndpoint(path) {
15460
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
15461
+ return `https://${SERVICE_NAME2}-proxy.${buildDomain()}${normalizedPath}`;
15462
+ }
15463
+ var UrlCheckClient = class {
15464
+ endpoint;
15465
+ timeoutMs;
15466
+ logger;
15467
+ constructor(config2, logger = nullLogger) {
15468
+ this.endpoint = config2?.endpoint ?? resolveEndpoint("/url-check");
15469
+ this.timeoutMs = (config2?.timeout_seconds ?? DEFAULT_TIMEOUT2) * 1e3;
15470
+ this.logger = logger;
15471
+ }
15472
+ async checkUrls(urls) {
15473
+ if (urls.length === 0)
15474
+ return [];
15475
+ const batches = [];
15476
+ for (let i = 0; i < urls.length; i += MAX_URLS_PER_REQUEST) {
15477
+ batches.push(urls.slice(i, i + MAX_URLS_PER_REQUEST));
15478
+ }
15479
+ const batchResults = await Promise.all(batches.map((batch) => this.checkBatch(batch)));
15480
+ return batchResults.flat();
15481
+ }
15482
+ async checkBatch(urls) {
15483
+ const queries = urls.map((url) => ({
15484
+ key: { "url-like": url },
15485
+ include: { "detection-infos": true }
15486
+ }));
15487
+ const payload = {
15488
+ queries,
15489
+ "client-info": {
15490
+ "product-name": SERVICE_NAME2,
15491
+ "product-version": VERSION
15492
+ }
15493
+ };
15494
+ try {
15495
+ const response = await fetch(this.endpoint, {
15496
+ method: "POST",
15497
+ headers: Object.fromEntries(REQUEST_HEADERS2.map((h) => [h.name, h.value])),
15498
+ body: JSON.stringify(payload),
15499
+ signal: AbortSignal.timeout(this.timeoutMs)
15500
+ });
15501
+ if (!response.ok) {
15502
+ this.logger.warn(`URL check HTTP error: ${response.status}`);
15503
+ return [];
15504
+ }
15505
+ const data = await response.json();
15506
+ const answers = data.answers ?? [];
15507
+ const results = [];
15508
+ for (const answer of answers) {
15509
+ const result = this.parseAnswer(answer);
15510
+ if (result !== null) {
15511
+ results.push(result);
15512
+ }
15513
+ }
15514
+ return results;
15515
+ } catch (e) {
15516
+ this.logger.warn("URL check request failed", { error: String(e) });
15517
+ return [];
15518
+ }
15519
+ }
15520
+ parseAnswer(answer) {
15521
+ try {
15522
+ const url = answer.key ?? "";
15523
+ const result = answer.result ?? {};
15524
+ const success = result.success ?? {};
15525
+ const classification = success.classification ?? {};
15526
+ const detectionInfos = classification["detection-infos"] ?? [];
15527
+ const classResult = classification.result ?? {};
15528
+ const malicious = classResult.malicious;
15529
+ const detections = detectionInfos.filter((info) => typeof info.name === "string").map((info) => {
15530
+ return info.name;
15531
+ });
15532
+ const findings = malicious ? (malicious.findings ?? []).map((f) => ({
15533
+ severityName: f["severity-name"] ?? "unknown",
15534
+ typeName: f["type-name"] ?? "unknown"
15535
+ })) : [];
15536
+ return {
15537
+ url,
15538
+ isMalicious: Boolean(malicious),
15539
+ detections,
15540
+ findings
15541
+ };
15542
+ } catch (e) {
15543
+ this.logger.warn("Failed to parse answer", { error: String(e) });
15544
+ return null;
15545
+ }
15546
+ }
15547
+ };
15548
+
15549
+ // ../core/dist/clients/package-registry.js
15550
+ var import_semver = __toESM(require_semver2(), 1);
15551
+ init_types2();
15552
+ var NPM_REGISTRY = "https://registry.npmjs.org";
15553
+ var PYPI_REGISTRY = "https://pypi.org/pypi";
15554
+ var DEFAULT_TIMEOUT3 = 5;
15555
+ var NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
15556
+ var PYPI_NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;
15557
+ var RegistryClient = class {
15558
+ timeoutMs;
15559
+ logger;
15560
+ constructor(config2, logger = nullLogger) {
15561
+ this.timeoutMs = (config2?.timeout_seconds ?? DEFAULT_TIMEOUT3) * 1e3;
15562
+ this.logger = logger;
15563
+ }
14833
15564
  async getPackageMetadata(name, registry, version) {
14834
15565
  if (registry === "npm")
14835
15566
  return this.getNpmMetadata(name, version);
@@ -14983,131 +15714,693 @@ var RegistryClient = class {
14983
15714
  return null;
14984
15715
  }
14985
15716
  }
14986
- };
15717
+ };
15718
+
15719
+ // ../core/dist/clients/pi-check.js
15720
+ var import_node_path9 = require("node:path");
15721
+ init_types2();
15722
+ var DEFAULT_MAX_CONTENT_LENGTH2 = 16384;
15723
+ var MAX_TOKENS = 512;
15724
+ var OVERLAP_TOKENS = 128;
15725
+ var MODEL_FILE = "model_int8.onnx";
15726
+ var MODEL_METADATA_FILES = [
15727
+ "config.json",
15728
+ "special_tokens_map.json",
15729
+ "tokenizer.json",
15730
+ "tokenizer_config.json",
15731
+ "vocab.txt",
15732
+ MODEL_FILE
15733
+ ];
15734
+ function softmax(logits) {
15735
+ const max = Math.max(...logits);
15736
+ const exp = logits.map((x) => Math.exp(x - max));
15737
+ const sum = exp.reduce((a, b) => a + b, 0);
15738
+ return exp.map((x) => x / sum);
15739
+ }
15740
+ function isModelMissingError(err) {
15741
+ if (!(err instanceof Error))
15742
+ return false;
15743
+ const nodeErr = err;
15744
+ if (nodeErr.code === "ENOENT")
15745
+ return true;
15746
+ const msg = err.message.toLowerCase();
15747
+ return msg.includes("enoent") || msg.includes("no such file or directory") || MODEL_METADATA_FILES.some((name) => msg.includes(name.toLowerCase()));
15748
+ }
15749
+ var BundledPiProvider = class _BundledPiProvider {
15750
+ /** Tracks whether any instance loaded the model runtime in this process. */
15751
+ static modelRuntimeLoaded = false;
15752
+ modelPath;
15753
+ maxContentLength;
15754
+ mediumRiskThreshold;
15755
+ logger;
15756
+ tokenizer = null;
15757
+ session = null;
15758
+ tensorClass = null;
15759
+ inputNames = /* @__PURE__ */ new Set();
15760
+ initPromise = null;
15761
+ /**
15762
+ * Call at the end of the process to prevent native model runtime cleanup
15763
+ * crash (exit code 134/SIGABRT). Only exits if the runtime was actually
15764
+ * loaded.
15765
+ */
15766
+ static exitIfModelLoaded() {
15767
+ if (_BundledPiProvider.modelRuntimeLoaded)
15768
+ process.exit(0);
15769
+ }
15770
+ constructor(options = {}) {
15771
+ this.modelPath = options.modelPath ?? getModelDir("pi-model");
15772
+ this.maxContentLength = options.maxContentLength ?? DEFAULT_MAX_CONTENT_LENGTH2;
15773
+ this.mediumRiskThreshold = options.mediumRiskThreshold ?? DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
15774
+ this.logger = options.logger ?? nullLogger;
15775
+ }
15776
+ async checkContent(content, context) {
15777
+ try {
15778
+ const truncated = this.truncateContent(content);
15779
+ if (truncated.length === 0)
15780
+ return null;
15781
+ const loaded = await this.ensureLoaded();
15782
+ if (!loaded)
15783
+ return null;
15784
+ const chunks = this.chunkText(truncated);
15785
+ let maxRisk = 0;
15786
+ let maxChunk = "";
15787
+ for (const chunk of chunks) {
15788
+ if (chunk.length < 10)
15789
+ continue;
15790
+ const risk = await this.classifyChunk(chunk);
15791
+ if (risk > maxRisk) {
15792
+ maxRisk = risk;
15793
+ maxChunk = chunk;
15794
+ }
15795
+ }
15796
+ const findings = [];
15797
+ if (maxRisk >= this.mediumRiskThreshold) {
15798
+ const snippet = maxChunk.length > 80 ? `${maxChunk.slice(0, 77)}...` : maxChunk;
15799
+ findings.push(snippet);
15800
+ }
15801
+ return {
15802
+ risk: maxRisk,
15803
+ findings,
15804
+ contentName: context,
15805
+ modelId: (0, import_node_path9.basename)(this.modelPath),
15806
+ contentSnippet: maxChunk || void 0
15807
+ };
15808
+ } catch (err) {
15809
+ this.logger.warn("PI check failed (fail-open)", {
15810
+ error: err instanceof Error ? err.message : String(err),
15811
+ context
15812
+ });
15813
+ return null;
15814
+ }
15815
+ }
15816
+ async ensureLoaded() {
15817
+ if (this.session && this.tokenizer)
15818
+ return true;
15819
+ if (this.initPromise)
15820
+ return this.initPromise;
15821
+ this.initPromise = this.loadModel();
15822
+ return this.initPromise;
15823
+ }
15824
+ async loadModel() {
15825
+ try {
15826
+ const { LocalTokenizer: LocalTokenizer2 } = await Promise.resolve().then(() => (init_tokenizer(), tokenizer_exports));
15827
+ this.tokenizer = LocalTokenizer2.fromModelDir(this.modelPath, MAX_TOKENS);
15828
+ const { ensurePiDeps: ensurePiDeps2 } = await Promise.resolve().then(() => (init_pi_deps_installer(), pi_deps_installer_exports));
15829
+ const depsReady = await ensurePiDeps2(this.modelPath, this.logger);
15830
+ if (!depsReady)
15831
+ return false;
15832
+ const { createRequire: createRequire2 } = await import("node:module");
15833
+ const requireFromModel = createRequire2((0, import_node_path9.resolve)(this.modelPath, "package.json"));
15834
+ const runtime = requireFromModel("onnxruntime-node");
15835
+ this.tensorClass = runtime.Tensor;
15836
+ const modelFilePath = (0, import_node_path9.resolve)(this.modelPath, MODEL_FILE);
15837
+ this.session = await runtime.InferenceSession.create(modelFilePath);
15838
+ const sess = this.session;
15839
+ this.inputNames = new Set(sess.inputNames);
15840
+ _BundledPiProvider.modelRuntimeLoaded = true;
15841
+ this.logger.info("PI model loaded", { path: this.modelPath });
15842
+ return true;
15843
+ } catch (err) {
15844
+ if (isModelMissingError(err)) {
15845
+ this.logger.debug("PI model not yet available; using heuristics only", {
15846
+ path: this.modelPath,
15847
+ error: err instanceof Error ? err.message : String(err)
15848
+ });
15849
+ return false;
15850
+ }
15851
+ const errMsg = err instanceof Error ? `${err.message}
15852
+ ${err.stack}` : String(err);
15853
+ this.logger.warn(`PI model load failed (pi_check is enabled but inference will be skipped): ${errMsg}`);
15854
+ return false;
15855
+ }
15856
+ }
15857
+ /**
15858
+ * Classify a single chunk. Returns P(injected) via softmax over two-class logits.
15859
+ */
15860
+ async classifyChunk(text) {
15861
+ const tok = this.tokenizer;
15862
+ const encoded = tok.call(text, {
15863
+ truncation: true,
15864
+ max_length: MAX_TOKENS,
15865
+ padding: true
15866
+ });
15867
+ const inputIds = new BigInt64Array(encoded.input_ids.map((v) => BigInt(v)));
15868
+ const attentionMask = new BigInt64Array(encoded.attention_mask.map((v) => BigInt(v)));
15869
+ const TensorClass = this.tensorClass;
15870
+ const feeds = {
15871
+ input_ids: new TensorClass("int64", inputIds, [1, inputIds.length]),
15872
+ attention_mask: new TensorClass("int64", attentionMask, [1, attentionMask.length])
15873
+ };
15874
+ if (this.inputNames.has("token_type_ids")) {
15875
+ feeds.token_type_ids = new TensorClass("int64", new BigInt64Array(inputIds.length), [
15876
+ 1,
15877
+ inputIds.length
15878
+ ]);
15879
+ }
15880
+ const session = this.session;
15881
+ const output = await session.run(feeds);
15882
+ const logits = Array.from(output.logits?.data ?? []);
15883
+ if (logits.length < 2)
15884
+ return 0;
15885
+ const probs = softmax(logits);
15886
+ return probs[1] ?? 0;
15887
+ }
15888
+ /**
15889
+ * Chunk text with a sliding window over tokenized input. Window size and
15890
+ * overlap are tuned to the model's input window.
15891
+ */
15892
+ chunkText(text) {
15893
+ const tok = this.tokenizer;
15894
+ const encoded = tok.tokenize(text, {
15895
+ addSpecialTokens: false,
15896
+ returnOffsetMapping: true,
15897
+ truncation: false
15898
+ });
15899
+ const tokenIds = encoded.input_ids;
15900
+ const offsets = encoded.offset_mapping;
15901
+ if (tokenIds.length <= MAX_TOKENS)
15902
+ return [text];
15903
+ const chunks = [];
15904
+ let start = 0;
15905
+ while (start < tokenIds.length) {
15906
+ const end = Math.min(start + MAX_TOKENS, tokenIds.length);
15907
+ const charStart = offsets[start]?.[0] ?? 0;
15908
+ const charEnd = offsets[end - 1]?.[1] ?? text.length;
15909
+ chunks.push(text.slice(charStart, charEnd));
15910
+ if (end >= tokenIds.length)
15911
+ break;
15912
+ start = end - OVERLAP_TOKENS;
15913
+ }
15914
+ return chunks;
15915
+ }
15916
+ truncateContent(content) {
15917
+ if (content.length <= this.maxContentLength)
15918
+ return content;
15919
+ const headLen = Math.floor(this.maxContentLength * 0.8);
15920
+ const tailLen = this.maxContentLength - headLen;
15921
+ return `${content.slice(0, headLen)}
15922
+ ...[truncated]...
15923
+ ${content.slice(-tailLen)}`;
15924
+ }
15925
+ };
15926
+
15927
+ // ../core/dist/index.js
15928
+ init_pi_deps_installer();
15929
+
15930
+ // ../core/dist/clients/skill-check.js
15931
+ init_types2();
15932
+ var DEFAULT_TIMEOUT4 = 5;
15933
+ var SERVICE_NAME3 = "sage";
15934
+ var MAX_IDS_PER_REQUEST = 50;
15935
+ var REQUEST_HEADERS3 = [
15936
+ { name: "Accept", value: "application/json" },
15937
+ { name: "Content-Type", value: "application/json" },
15938
+ { name: "User-Agent", value: SERVICE_NAME3 }
15939
+ ];
15940
+ var SkillCheckClient = class {
15941
+ endpoint;
15942
+ timeoutMs;
15943
+ logger;
15944
+ constructor(config2, logger = nullLogger) {
15945
+ this.endpoint = config2?.endpoint ?? resolveEndpoint("/v2/skill-check");
15946
+ this.timeoutMs = (config2?.timeout_seconds ?? DEFAULT_TIMEOUT4) * 1e3;
15947
+ this.logger = logger;
15948
+ }
15949
+ /**
15950
+ * Check a list of skill IDs against the proxy.
15951
+ *
15952
+ * Returns a Map keyed by skill id. A `null` value means the proxy had
15953
+ * no opinion on that skill (analogous to a clean verdict). Skill ids
15954
+ * not found in the response are simply absent from the map.
15955
+ *
15956
+ * Fails-open: on any error returns an empty Map. Per-batch errors do
15957
+ * not poison results from successful batches.
15958
+ */
15959
+ async checkSkills(skillIds) {
15960
+ const out = /* @__PURE__ */ new Map();
15961
+ if (skillIds.length === 0)
15962
+ return out;
15963
+ const unique = [...new Set(skillIds)];
15964
+ const batches = [];
15965
+ for (let i = 0; i < unique.length; i += MAX_IDS_PER_REQUEST) {
15966
+ batches.push(unique.slice(i, i + MAX_IDS_PER_REQUEST));
15967
+ }
15968
+ const batchResults = await Promise.all(batches.map((batch) => this.checkBatch(batch)));
15969
+ for (const batch of batchResults) {
15970
+ for (const [id, result] of batch) {
15971
+ out.set(id, result);
15972
+ }
15973
+ }
15974
+ return out;
15975
+ }
15976
+ async checkBatch(skillIds) {
15977
+ const out = /* @__PURE__ */ new Map();
15978
+ try {
15979
+ const response = await fetch(this.endpoint, {
15980
+ method: "POST",
15981
+ headers: Object.fromEntries(REQUEST_HEADERS3.map((h) => [h.name, h.value])),
15982
+ body: JSON.stringify({
15983
+ skill_ids: skillIds,
15984
+ client_info: {
15985
+ product_name: SERVICE_NAME3,
15986
+ product_version: VERSION
15987
+ }
15988
+ }),
15989
+ signal: AbortSignal.timeout(this.timeoutMs)
15990
+ });
15991
+ if (!response.ok) {
15992
+ this.logger.warn(`SkillCheck HTTP error: ${response.status}`);
15993
+ return out;
15994
+ }
15995
+ const data = await response.json();
15996
+ const results = data.results ?? {};
15997
+ for (const id of skillIds) {
15998
+ if (!(id in results))
15999
+ continue;
16000
+ const raw = results[id];
16001
+ if (raw === null || raw === void 0) {
16002
+ out.set(id, null);
16003
+ continue;
16004
+ }
16005
+ const parsed = this.parseResult(id, raw);
16006
+ out.set(id, parsed);
16007
+ }
16008
+ } catch (e) {
16009
+ this.logger.warn("SkillCheck batch request failed", { error: String(e) });
16010
+ }
16011
+ return out;
16012
+ }
16013
+ parseResult(skillId, raw) {
16014
+ const recommendationsRaw = raw.recommendations ?? [];
16015
+ const recommendations = recommendationsRaw.filter((r) => typeof r === "string");
16016
+ const categoriesRaw = raw.threat_categories ?? [];
16017
+ const threatCategories = categoriesRaw.filter((c) => typeof c === "string");
16018
+ return {
16019
+ skillId,
16020
+ verdict: typeof raw.verdict === "string" ? raw.verdict : void 0,
16021
+ overallRiskLevel: typeof raw.overall_risk_level === "string" ? raw.overall_risk_level : void 0,
16022
+ summary: typeof raw.summary === "string" ? raw.summary : void 0,
16023
+ recommendations,
16024
+ threatCategories
16025
+ };
16026
+ }
16027
+ };
16028
+
16029
+ // ../core/dist/index.js
16030
+ init_config();
16031
+
16032
+ // ../core/dist/content-policy.js
16033
+ var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
16034
+ ".py",
16035
+ ".js",
16036
+ ".mjs",
16037
+ ".cjs",
16038
+ ".jsx",
16039
+ ".ts",
16040
+ ".tsx",
16041
+ ".mts",
16042
+ ".cts",
16043
+ ".sh",
16044
+ ".bash",
16045
+ ".html",
16046
+ ".htm",
16047
+ ".json",
16048
+ ".jsonc",
16049
+ ".txt",
16050
+ ".csv",
16051
+ ".xml",
16052
+ ".c",
16053
+ ".h",
16054
+ ".cpp",
16055
+ ".cc",
16056
+ ".cxx",
16057
+ ".hpp",
16058
+ ".yaml",
16059
+ ".yml",
16060
+ ".kt",
16061
+ ".kts"
16062
+ ]);
16063
+ function getUrlExtension(url) {
16064
+ try {
16065
+ const pathname = new URL(url).pathname;
16066
+ const dot = pathname.lastIndexOf(".");
16067
+ const slash = pathname.lastIndexOf("/");
16068
+ if (dot <= slash || dot < 0)
16069
+ return null;
16070
+ return pathname.slice(dot).toLowerCase();
16071
+ } catch {
16072
+ return null;
16073
+ }
16074
+ }
16075
+ function extractWebFetchUrl(toolInput) {
16076
+ if (typeof toolInput.url === "string")
16077
+ return toolInput.url;
16078
+ if (Array.isArray(toolInput.urls) && typeof toolInput.urls[0] === "string")
16079
+ return toolInput.urls[0];
16080
+ return null;
16081
+ }
16082
+ var SNIFF_HTML = /^\s*<!doctype\s+html|^\s*<html[\s>]/i;
16083
+ var SNIFF_HTML_TAGS = /<\/(head|body|html|div|p|script|style|form|table)>/i;
16084
+ var SNIFF_JSON = /^\s*[[{]/;
16085
+ var SNIFF_XML = /^\s*<\?xml\s/i;
16086
+ var SNIFF_YAML = /^---\s*$/m;
16087
+ var SNIFF_SHEBANG_SHELL = /^#!.*\b(sh|bash|zsh|dash)\b/;
16088
+ var SNIFF_SHEBANG_PYTHON = /^#!.*\bpython/i;
16089
+ var SNIFF_PYTHON = /^(?:import |from \S+ import |def \w+\(|class \w+[:(])/m;
16090
+ var SNIFF_JS_TS = /^(?:const |let |var |function |import |export |module\.exports)/m;
16091
+ var SNIFF_C_CPP = /^#include\s*[<"]/m;
16092
+ var SNIFF_KOTLIN = /^(?:package |fun |class |val |var |object |interface )\w/m;
16093
+ var SNIFF_CSV = /^[^,\n]+(?:,[^,\n]+){2,}$/m;
16094
+ function isScannableContent(content) {
16095
+ const head = content.slice(0, 4096);
16096
+ 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);
16097
+ }
16098
+
16099
+ // ../core/dist/content-snapshot.js
16100
+ var import_node_os2 = require("node:os");
16101
+ var CONTENT_FIELD_LIMITS = Object.freeze({
16102
+ command: 512,
16103
+ url: 512,
16104
+ file_path: 512,
16105
+ package_name: 256,
16106
+ package_version: 128,
16107
+ package_registry: 128
16108
+ });
16109
+ function safeTruncate(value, maxLen) {
16110
+ if (maxLen <= 0)
16111
+ return "";
16112
+ if (value.length <= maxLen)
16113
+ return value;
16114
+ const cutIndex = maxLen;
16115
+ const codeUnit = value.charCodeAt(cutIndex - 1);
16116
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
16117
+ return value.slice(0, cutIndex - 1);
16118
+ }
16119
+ return value.slice(0, cutIndex);
16120
+ }
16121
+ function scrubHomePath(value) {
16122
+ const home = (0, import_node_os2.homedir)();
16123
+ if (!home)
16124
+ return value;
16125
+ const normalizedHome = home.replace(/\\/g, "/").replace(/\/+$/, "");
16126
+ if (!normalizedHome)
16127
+ return value;
16128
+ const normalizedValue = value.replace(/\\/g, "/");
16129
+ if (normalizedValue === normalizedHome)
16130
+ return "~";
16131
+ if (normalizedValue.startsWith(`${normalizedHome}/`)) {
16132
+ return `~/${normalizedValue.slice(normalizedHome.length + 1)}`;
16133
+ }
16134
+ return value;
16135
+ }
16136
+ function asString(v) {
16137
+ return typeof v === "string" && v.length > 0 ? v : void 0;
16138
+ }
16139
+ function resolveFilePath(toolInput) {
16140
+ return asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
16141
+ }
16142
+ function resolveWebFetchUrl(toolInput) {
16143
+ const single = asString(toolInput.url);
16144
+ if (single)
16145
+ return single;
16146
+ const urls = toolInput.urls;
16147
+ if (Array.isArray(urls)) {
16148
+ for (const u of urls) {
16149
+ if (typeof u === "string" && u.length > 0)
16150
+ return u;
16151
+ }
16152
+ }
16153
+ return void 0;
16154
+ }
16155
+ function extractFirstApplyPatchPath(patchText) {
16156
+ if (!patchText)
16157
+ return void 0;
16158
+ const headerMatch = /\*{3}\s+(?:Add|Update|Delete)\s+File:\s*(.+)/.exec(patchText);
16159
+ if (headerMatch?.[1]) {
16160
+ const trimmed = headerMatch[1].trim();
16161
+ if (trimmed)
16162
+ return trimmed;
16163
+ }
16164
+ const renameMatch = /\*{3}\s+(?:Move\s+to|Rename\s+File):\s*(.+)/i.exec(patchText);
16165
+ if (renameMatch?.[1]) {
16166
+ const raw = renameMatch[1].trim();
16167
+ const arrow = raw.indexOf(" -> ");
16168
+ if (arrow !== -1) {
16169
+ const src = raw.slice(0, arrow).trim();
16170
+ if (src)
16171
+ return src;
16172
+ const dst = raw.slice(arrow + 4).trim();
16173
+ if (dst)
16174
+ return dst;
16175
+ } else if (raw) {
16176
+ return raw;
16177
+ }
16178
+ }
16179
+ return void 0;
16180
+ }
16181
+ function extractMcpContent(toolInput) {
16182
+ const nestedRaw = toolInput.tool_input ?? toolInput.toolInput;
16183
+ const candidate = nestedRaw && typeof nestedRaw === "object" && !Array.isArray(nestedRaw) ? nestedRaw : toolInput;
16184
+ return {
16185
+ command: asString(candidate.command),
16186
+ url: asString(candidate.url) ?? resolveWebFetchUrl(candidate),
16187
+ file_path: resolveFilePath(candidate)
16188
+ };
16189
+ }
16190
+ function firstUrlArtifact(artifacts) {
16191
+ for (const a of artifacts) {
16192
+ if (a.type === "url" && typeof a.value === "string" && a.value.length > 0) {
16193
+ return a.value;
16194
+ }
16195
+ }
16196
+ return void 0;
16197
+ }
16198
+ function applyFieldLimits(content) {
16199
+ for (const [key, value] of Object.entries(content)) {
16200
+ if (typeof value !== "string")
16201
+ continue;
16202
+ let sanitized = value;
16203
+ if (key === "file_path" || key === "command") {
16204
+ sanitized = scrubHomePath(sanitized);
16205
+ }
16206
+ const limit = CONTENT_FIELD_LIMITS[key];
16207
+ if (limit && sanitized.length > limit) {
16208
+ sanitized = safeTruncate(sanitized, limit);
16209
+ }
16210
+ content[key] = sanitized;
16211
+ }
16212
+ }
16213
+ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {}) {
16214
+ const content = {};
16215
+ switch (toolType) {
16216
+ case "Bash": {
16217
+ const command = asString(toolInput.command);
16218
+ if (command)
16219
+ content.command = command;
16220
+ break;
16221
+ }
16222
+ case "WebFetch": {
16223
+ const url = resolveWebFetchUrl(toolInput) ?? firstUrlArtifact(artifacts);
16224
+ if (url)
16225
+ content.url = url;
16226
+ break;
16227
+ }
16228
+ case "Write":
16229
+ case "Edit":
16230
+ case "Read":
16231
+ case "Delete": {
16232
+ const filePath = resolveFilePath(toolInput);
16233
+ if (filePath)
16234
+ content.file_path = filePath;
16235
+ break;
16236
+ }
16237
+ case "ApplyPatch": {
16238
+ const patchText = asString(toolInput.input) ?? asString(toolInput.patch) ?? "";
16239
+ const filePath = extractFirstApplyPatchPath(patchText);
16240
+ if (filePath)
16241
+ content.file_path = filePath;
16242
+ break;
16243
+ }
16244
+ case "MCP": {
16245
+ const mcp = extractMcpContent(toolInput);
16246
+ if (mcp.command)
16247
+ content.command = mcp.command;
16248
+ if (mcp.url)
16249
+ content.url = mcp.url;
16250
+ if (mcp.file_path)
16251
+ content.file_path = mcp.file_path;
16252
+ break;
16253
+ }
16254
+ }
16255
+ if (!content.url && signals.url_checks && signals.url_checks.length > 0) {
16256
+ const firstUrl = signals.url_checks[0]?.url;
16257
+ if (firstUrl)
16258
+ content.url = firstUrl;
16259
+ }
16260
+ if (signals.package_checks && signals.package_checks.length > 0) {
16261
+ const p = signals.package_checks[0];
16262
+ if (p?.package_name)
16263
+ content.package_name = p.package_name;
16264
+ if (p?.package_version)
16265
+ content.package_version = p.package_version;
16266
+ if (p?.package_registry)
16267
+ content.package_registry = p.package_registry;
16268
+ }
16269
+ applyFieldLimits(content);
16270
+ return content;
16271
+ }
14987
16272
 
14988
- // ../core/dist/clients/url-check.js
16273
+ // ../core/dist/extended-info.js
16274
+ var import_promises2 = require("node:fs/promises");
16275
+ var import_node_os3 = require("node:os");
16276
+ var import_node_path10 = require("node:path");
16277
+ init_file_utils();
14989
16278
  init_types2();
14990
- var DEFAULT_TIMEOUT3 = 5;
14991
- var MAX_URLS_PER_REQUEST = 50;
14992
- var SERVICE_NAME2 = "sage";
14993
- function getProviderTld2() {
14994
- return "com";
14995
- }
14996
- var REQUEST_HEADERS2 = [
14997
- { name: "Accept", value: "application/json" },
14998
- { name: "Content-Type", value: "application/json" },
14999
- { name: "User-Agent", value: SERVICE_NAME2 }
15000
- ];
15001
- function getProviderName2() {
15002
- return "avast";
16279
+ var EXTENDED_INFO_FILE_MAX_BYTES = 1024;
16280
+ var EXTENDED_INFO_MAX_GROUPS = 16;
16281
+ var EXTENDED_INFO_MAX_KEYS_PER_GROUP = 32;
16282
+ var EXTENDED_INFO_MAX_LEAF_CHARS = 256;
16283
+ var EXTENDED_INFO_FILENAME = "extended-info.json";
16284
+ var cache = /* @__PURE__ */ new Map();
16285
+ function isPlainObjectValue(value) {
16286
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15003
16287
  }
15004
- function getSubdomain() {
15005
- return "svc";
16288
+ function sanitizeLeaves(groupKey, rawGroup, logger) {
16289
+ const entries = Object.entries(rawGroup);
16290
+ const overflow = entries.length > EXTENDED_INFO_MAX_KEYS_PER_GROUP;
16291
+ const retained = overflow ? entries.slice(0, EXTENDED_INFO_MAX_KEYS_PER_GROUP) : entries;
16292
+ if (overflow) {
16293
+ logger.debug(`extended-info: dropped overflow keys in group '${groupKey}' (kept first ${EXTENDED_INFO_MAX_KEYS_PER_GROUP} of ${entries.length})`);
16294
+ }
16295
+ const out = {};
16296
+ for (const [leafKey, leafValue] of retained) {
16297
+ if (typeof leafValue === "string") {
16298
+ out[leafKey] = safeTruncate(leafValue, EXTENDED_INFO_MAX_LEAF_CHARS);
16299
+ continue;
16300
+ }
16301
+ if (typeof leafValue === "number" || typeof leafValue === "boolean") {
16302
+ out[leafKey] = leafValue;
16303
+ continue;
16304
+ }
16305
+ logger.debug(`extended-info: dropped non-scalar leaf '${groupKey}.${leafKey}' (type ${typeof leafValue})`);
16306
+ }
16307
+ return out;
15006
16308
  }
15007
- function buildDomain() {
15008
- return [getSubdomain(), getProviderName2(), getProviderTld2()].join(".");
16309
+ function sanitizeDocument(parsed, logger) {
16310
+ const out = {};
16311
+ for (const [groupKey, groupValue] of Object.entries(parsed)) {
16312
+ if (!isPlainObjectValue(groupValue)) {
16313
+ logger.debug(`extended-info: dropped non-object group '${groupKey}'`);
16314
+ continue;
16315
+ }
16316
+ out[groupKey] = sanitizeLeaves(groupKey, groupValue, logger);
16317
+ }
16318
+ return out;
15009
16319
  }
15010
- function resolveEndpoint(path) {
15011
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
15012
- return `https://${SERVICE_NAME2}-proxy.${buildDomain()}${normalizedPath}`;
16320
+ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
16321
+ const sageDir = sageDirPath ?? (0, import_node_path10.join)((0, import_node_os3.homedir)(), ".sage");
16322
+ const filePath = (0, import_node_path10.join)(sageDir, EXTENDED_INFO_FILENAME);
16323
+ const cached = cache.get(filePath);
16324
+ if (cached)
16325
+ return cached.value;
16326
+ const value = await loadExtendedInfoUncached(filePath, logger);
16327
+ cache.set(filePath, { value });
16328
+ return value;
15013
16329
  }
15014
- var UrlCheckClient = class {
15015
- endpoint;
15016
- timeoutMs;
15017
- logger;
15018
- constructor(config, logger = nullLogger) {
15019
- this.endpoint = config?.endpoint ?? resolveEndpoint("/url-check");
15020
- this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT3) * 1e3;
15021
- this.logger = logger;
15022
- }
15023
- async checkUrls(urls) {
15024
- if (urls.length === 0)
15025
- return [];
15026
- const batches = [];
15027
- for (let i = 0; i < urls.length; i += MAX_URLS_PER_REQUEST) {
15028
- batches.push(urls.slice(i, i + MAX_URLS_PER_REQUEST));
16330
+ async function loadExtendedInfoUncached(filePath, logger) {
16331
+ let size;
16332
+ try {
16333
+ const info = await (0, import_promises2.stat)(filePath);
16334
+ if (!info.isFile()) {
16335
+ logger.debug(`extended-info: not a regular file at ${filePath}`);
16336
+ return null;
15029
16337
  }
15030
- const batchResults = await Promise.all(batches.map((batch) => this.checkBatch(batch)));
15031
- return batchResults.flat();
16338
+ size = info.size;
16339
+ } catch {
16340
+ return null;
15032
16341
  }
15033
- async checkBatch(urls) {
15034
- const queries = urls.map((url) => ({ key: { "url-like": url } }));
15035
- const payload = {
15036
- queries,
15037
- "client-info": {
15038
- "product-name": SERVICE_NAME2,
15039
- "product-version": VERSION
15040
- }
15041
- };
15042
- try {
15043
- const response = await fetch(this.endpoint, {
15044
- method: "POST",
15045
- headers: Object.fromEntries(REQUEST_HEADERS2.map((h) => [h.name, h.value])),
15046
- body: JSON.stringify(payload),
15047
- signal: AbortSignal.timeout(this.timeoutMs)
15048
- });
15049
- if (!response.ok) {
15050
- this.logger.warn(`URL check HTTP error: ${response.status}`);
15051
- return [];
15052
- }
15053
- const data = await response.json();
15054
- const answers = data.answers ?? [];
15055
- const results = [];
15056
- for (const answer of answers) {
15057
- const result = this.parseAnswer(answer);
15058
- if (result !== null) {
15059
- results.push(result);
15060
- }
15061
- }
15062
- return results;
15063
- } catch (e) {
15064
- this.logger.warn("URL check request failed", { error: String(e) });
15065
- return [];
15066
- }
16342
+ if (size > EXTENDED_INFO_FILE_MAX_BYTES) {
16343
+ logger.debug(`extended-info: file size ${size} exceeds cap ${EXTENDED_INFO_FILE_MAX_BYTES}; ignoring`);
16344
+ return null;
15067
16345
  }
15068
- parseAnswer(answer) {
15069
- try {
15070
- const url = answer.key ?? "";
15071
- const result = answer.result ?? {};
15072
- const success = result.success ?? {};
15073
- const classification = success.classification ?? {};
15074
- const detectionInfos = classification["detection-infos"] ?? [];
15075
- const classResult = classification.result ?? {};
15076
- const flags = success.flags ?? [];
15077
- const malicious = classResult.malicious;
15078
- const detections = detectionInfos.filter((info) => typeof info.name === "string").map((info) => {
15079
- return info.name;
15080
- });
15081
- const findings = malicious ? (malicious.findings ?? []).map((f) => ({
15082
- severityName: f["severity-name"] ?? "unknown",
15083
- typeName: f["type-name"] ?? "unknown"
15084
- })) : [];
15085
- return {
15086
- url,
15087
- isMalicious: Boolean(malicious),
15088
- detections,
15089
- findings,
15090
- flags
15091
- };
15092
- } catch (e) {
15093
- this.logger.warn("Failed to parse answer", { error: String(e) });
15094
- return null;
16346
+ let raw;
16347
+ try {
16348
+ raw = await getFileContent(filePath, "utf-8");
16349
+ } catch {
16350
+ return null;
16351
+ }
16352
+ let parsed;
16353
+ try {
16354
+ parsed = JSON.parse(raw);
16355
+ } catch (err) {
16356
+ logger.debug(`extended-info: invalid JSON: ${err}`);
16357
+ return null;
16358
+ }
16359
+ if (!isPlainObjectValue(parsed)) {
16360
+ logger.debug("extended-info: top-level value is not a non-null object");
16361
+ return null;
16362
+ }
16363
+ const groupCount = Object.keys(parsed).length;
16364
+ if (groupCount > EXTENDED_INFO_MAX_GROUPS) {
16365
+ logger.debug(`extended-info: ${groupCount} top-level groups exceed cap ${EXTENDED_INFO_MAX_GROUPS}; rejecting whole file`);
16366
+ return null;
16367
+ }
16368
+ return sanitizeDocument(parsed, logger);
16369
+ }
16370
+ function mergeExtendedInfo(envelope, extendedInfo) {
16371
+ const out = { ...envelope };
16372
+ if (!extendedInfo)
16373
+ return out;
16374
+ for (const [groupKey, groupValue] of Object.entries(extendedInfo)) {
16375
+ const existing = out[groupKey];
16376
+ if (existing === void 0 || existing === null) {
16377
+ out[groupKey] = { ...groupValue };
16378
+ continue;
16379
+ }
16380
+ if (!isPlainObjectValue(existing)) {
16381
+ continue;
16382
+ }
16383
+ const merged = { ...existing };
16384
+ for (const [leafKey, leafValue] of Object.entries(groupValue)) {
16385
+ const current = merged[leafKey];
16386
+ if (current === void 0 || current === null) {
16387
+ merged[leafKey] = leafValue;
16388
+ }
15095
16389
  }
16390
+ out[groupKey] = merged;
15096
16391
  }
15097
- };
15098
-
15099
- // ../core/dist/index.js
15100
- init_config();
16392
+ return out;
16393
+ }
15101
16394
 
15102
16395
  // ../core/dist/installation-id.js
15103
16396
  var import_node_crypto5 = require("node:crypto");
15104
- var import_promises2 = require("node:fs/promises");
15105
- var import_node_path7 = require("node:path");
16397
+ var import_promises3 = require("node:fs/promises");
16398
+ var import_node_path11 = require("node:path");
15106
16399
  init_config();
15107
16400
  init_file_utils();
15108
16401
  async function getInstallationId(sageDirPath) {
15109
16402
  const sageDir = sageDirPath ?? resolvePath("~/.sage");
15110
- const idPath = (0, import_node_path7.join)(sageDir, "installation-id");
16403
+ const idPath = (0, import_node_path11.join)(sageDir, "installation-id");
15111
16404
  let fileExists = false;
15112
16405
  try {
15113
16406
  const existing = await getFileContent(idPath, "utf-8");
@@ -15119,8 +16412,8 @@ async function getInstallationId(sageDirPath) {
15119
16412
  }
15120
16413
  try {
15121
16414
  const id = (0, import_node_crypto5.randomUUID)();
15122
- await (0, import_promises2.mkdir)(sageDir, { recursive: true, mode: 448 });
15123
- await (0, import_promises2.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
16415
+ await (0, import_promises3.mkdir)(sageDir, { recursive: true, mode: 448 });
16416
+ await (0, import_promises3.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
15124
16417
  return id;
15125
16418
  } catch (err) {
15126
16419
  if (err.code === "EEXIST") {
@@ -15135,146 +16428,17 @@ async function getInstallationId(sageDirPath) {
15135
16428
  }
15136
16429
  }
15137
16430
 
15138
- // ../core/dist/sage-proxy.js
15139
- function mapSageProxyOs(platform) {
15140
- switch (platform) {
15141
- case "win32":
15142
- return "WINDOWS";
15143
- case "darwin":
15144
- return "MACOS";
15145
- case "linux":
15146
- return "LINUX";
15147
- default:
15148
- return platform;
15149
- }
15150
- }
15151
- function mapSageProxyArchitecture(arch) {
15152
- return arch.toUpperCase();
15153
- }
15154
- function buildSageProxyEnvelope(args) {
15155
- return {
15156
- identity: { uuid: args.iid },
15157
- product: { version_app: args.versionApp },
15158
- platform: {
15159
- os: args.platformOs ?? mapSageProxyOs(process.platform),
15160
- architecture: args.platformArchitecture ?? mapSageProxyArchitecture(process.arch)
15161
- },
15162
- agent: {
15163
- agent_runtime: args.agentRuntime,
15164
- agent_runtime_version: args.agentRuntimeVersion
15165
- }
15166
- };
15167
- }
15168
-
15169
16431
  // ../core/dist/detection-telemetry.js
15170
16432
  init_types2();
15171
16433
  var MAX_EFFECTIVE_TIMEOUT_MS = 1e3;
15172
- var DEFAULT_TIMEOUT_MS = 1e3;
15173
- var OPENCLAW_MAP = {
15174
- bash: "Bash",
15175
- exec: "Bash",
15176
- web_fetch: "WebFetch",
15177
- write: "Write",
15178
- edit: "Edit",
15179
- read: "Read",
15180
- apply_patch: "ApplyPatch"
15181
- };
15182
- var OPENCODE_MAP = {
15183
- bash: "Bash",
15184
- webfetch: "WebFetch",
15185
- write: "Write",
15186
- edit: "Edit",
15187
- read: "Read",
15188
- glob: "Glob",
15189
- grep: "Grep",
15190
- ls: "List",
15191
- codesearch: "CodeSearch",
15192
- websearch: "WebSearch",
15193
- question: "Question",
15194
- task: "Task",
15195
- read_lines: "ReadLines"
15196
- };
15197
- var CANONICAL_SET = /* @__PURE__ */ new Set([
15198
- "Bash",
15199
- "WebFetch",
15200
- "Write",
15201
- "Edit",
15202
- "Read",
15203
- "Delete",
15204
- "ApplyPatch",
15205
- "Glob",
15206
- "Grep",
15207
- "List",
15208
- "CodeSearch",
15209
- "WebSearch",
15210
- "Question",
15211
- "Task",
15212
- "ReadLines",
15213
- "MCP",
15214
- "Unknown"
15215
- ]);
15216
- function normalizeDetectionTelemetryContext(agentRuntime, toolName, toolInput) {
15217
- const toolType = canonicalizeToolName(agentRuntime, toolName);
15218
- const content = buildContent(toolType, toolInput);
15219
- return { toolType, content };
15220
- }
15221
- function canonicalizeToolName(agentRuntime, toolName) {
15222
- if (CANONICAL_SET.has(toolName))
15223
- return toolName;
15224
- if (agentRuntime === "openclaw") {
15225
- return OPENCLAW_MAP[toolName] ?? "Unknown";
15226
- }
15227
- if (agentRuntime === "opencode") {
15228
- return OPENCODE_MAP[toolName] ?? "Unknown";
15229
- }
15230
- return "Unknown";
15231
- }
15232
- function asString(v) {
15233
- return typeof v === "string" ? v : void 0;
15234
- }
15235
- function buildContent(toolType, toolInput) {
15236
- const content = {};
15237
- switch (toolType) {
15238
- case "Bash": {
15239
- const command = asString(toolInput.command);
15240
- if (command)
15241
- content.command = command;
15242
- break;
15243
- }
15244
- case "WebFetch": {
15245
- const url = asString(toolInput.url);
15246
- if (url)
15247
- content.url = url;
15248
- break;
15249
- }
15250
- case "Write":
15251
- case "Edit":
15252
- case "Read":
15253
- case "Delete": {
15254
- const filePath = asString(toolInput.file_path) ?? asString(toolInput.filePath) ?? asString(toolInput.path);
15255
- if (filePath)
15256
- content.file_path = filePath;
15257
- break;
15258
- }
15259
- }
15260
- const packageName = asString(toolInput.package_name);
15261
- if (packageName)
15262
- content.package_name = packageName;
15263
- const packageVersion = asString(toolInput.package_version);
15264
- if (packageVersion)
15265
- content.package_version = packageVersion;
15266
- const packageRegistry = asString(toolInput.package_registry);
15267
- if (packageRegistry)
15268
- content.package_registry = packageRegistry;
15269
- return content;
15270
- }
16434
+ var DEFAULT_TIMEOUT_MS2 = 1e3;
15271
16435
  function resolveTimeoutMs() {
15272
16436
  const envVal = process.env.SAGE_COMMUNITY_IQ_TIMEOUT_SECONDS;
15273
16437
  if (envVal === void 0 || envVal === "")
15274
- return DEFAULT_TIMEOUT_MS;
16438
+ return DEFAULT_TIMEOUT_MS2;
15275
16439
  const parsed = Number.parseFloat(envVal);
15276
16440
  if (!Number.isFinite(parsed) || parsed <= 0)
15277
- return DEFAULT_TIMEOUT_MS;
16441
+ return DEFAULT_TIMEOUT_MS2;
15278
16442
  const ms = Math.floor(parsed * 1e3);
15279
16443
  return Math.min(ms, MAX_EFFECTIVE_TIMEOUT_MS);
15280
16444
  }
@@ -15293,7 +16457,6 @@ async function sendCommunityIqDetection(args) {
15293
16457
  logger.debug("Skipping detection telemetry: missing installation id");
15294
16458
  return;
15295
16459
  }
15296
- const { toolType, content } = normalizeDetectionTelemetryContext(args.agentRuntime, args.toolName, args.toolInput);
15297
16460
  const envelope = buildSageProxyEnvelope({
15298
16461
  iid,
15299
16462
  versionApp: VERSION,
@@ -15304,22 +16467,27 @@ async function sendCommunityIqDetection(args) {
15304
16467
  ...envelope,
15305
16468
  block_event: {
15306
16469
  hook_type: args.hookType ?? "PreToolUse",
15307
- tool_type: toolType,
16470
+ // `args.toolName` is canonical (`CanonicalToolType`) — connectors
16471
+ // canonicalize before calling `evaluateToolCall`, so no further
16472
+ // mapping is needed here.
16473
+ tool_type: args.toolName,
15308
16474
  verdict: "deny",
15309
16475
  user_action: "blocked",
15310
16476
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
15311
16477
  ...args.signals && Object.keys(args.signals).length > 0 ? { signals: args.signals } : {},
15312
- content
16478
+ content: args.content ?? {}
15313
16479
  },
15314
16480
  event_id: args.eventId,
15315
16481
  comment: ""
15316
16482
  };
16483
+ const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
16484
+ const enriched = mergeExtendedInfo(payload, extendedInfo);
15317
16485
  const timeoutMs = resolveTimeoutMs();
15318
16486
  try {
15319
16487
  const response = await fetch(resolveEndpoint("/v2/detection"), {
15320
16488
  method: "POST",
15321
16489
  headers: { "Content-Type": "application/json" },
15322
- body: JSON.stringify(payload),
16490
+ body: JSON.stringify(enriched),
15323
16491
  signal: AbortSignal.timeout(timeoutMs)
15324
16492
  });
15325
16493
  if (!response.ok) {
@@ -15331,6 +16499,7 @@ async function sendCommunityIqDetection(args) {
15331
16499
  }
15332
16500
 
15333
16501
  // ../core/dist/engine.js
16502
+ init_types2();
15334
16503
  var CONFIDENCE_THRESHOLD = 0.85;
15335
16504
  var SENSITIVITY_THRESHOLDS = {
15336
16505
  paranoid: 0.7,
@@ -15353,13 +16522,44 @@ var DECISION_PRIORITY = {
15353
16522
  ask: 1,
15354
16523
  deny: 2
15355
16524
  };
16525
+ var PI_LABEL_ONLY_SUFFIXES = /* @__PURE__ */ new Set([
16526
+ "command",
16527
+ "stdout",
16528
+ "stderr",
16529
+ "output"
16530
+ ]);
16531
+ function displayContentName(contentName) {
16532
+ const colonIdx = contentName.indexOf(":");
16533
+ if (colonIdx === -1)
16534
+ return contentName;
16535
+ const rest = contentName.slice(colonIdx + 1);
16536
+ if (PI_LABEL_ONLY_SUFFIXES.has(rest))
16537
+ return contentName;
16538
+ const trimmed = rest.replace(/[/\\]+$/, "");
16539
+ const sepIdx = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
16540
+ const base = sepIdx === -1 ? trimmed : trimmed.slice(sepIdx + 1);
16541
+ return base || rest;
16542
+ }
16543
+ function buildPiReason(prefix, result) {
16544
+ return `${prefix} in ${formatPiFinding(result)}`;
16545
+ }
16546
+ function formatPiFinding(result) {
16547
+ const where = displayContentName(result.contentName);
16548
+ const score = `score: ${result.risk.toFixed(3)}`;
16549
+ const snippet = result.findings[0]?.replace(/\s+/g, " ").trim();
16550
+ if (snippet)
16551
+ return `${where} (${score}): "${snippet}"`;
16552
+ return `${where} (${score})`;
16553
+ }
15356
16554
  var DecisionEngine = class {
15357
16555
  threshold;
16556
+ sensitivity;
15358
16557
  constructor(sensitivity = "balanced") {
15359
16558
  this.threshold = SENSITIVITY_THRESHOLDS[sensitivity] ?? CONFIDENCE_THRESHOLD;
16559
+ this.sensitivity = sensitivity;
15360
16560
  }
15361
16561
  async decide(sources) {
15362
- const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults);
16562
+ const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds);
15363
16563
  if (signals.length === 0) {
15364
16564
  return this.allowVerdict();
15365
16565
  }
@@ -15372,7 +16572,7 @@ var DecisionEngine = class {
15372
16572
  const allReasons = [...new Map(signals.map((s) => [s.reason, s.reason])).values()];
15373
16573
  const maxConfidence = Math.max(...signals.map((s) => s.confidence));
15374
16574
  let decision = top.decision;
15375
- if (decision === "deny" && maxConfidence < this.threshold) {
16575
+ if (decision === "deny" && maxConfidence < this.threshold && top.source !== "pi_check") {
15376
16576
  decision = "ask";
15377
16577
  }
15378
16578
  return {
@@ -15386,7 +16586,7 @@ var DecisionEngine = class {
15386
16586
  reasons: allReasons
15387
16587
  };
15388
16588
  }
15389
- collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults) {
16589
+ collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults, piCheckResults, piThresholds) {
15390
16590
  const signals = [];
15391
16591
  for (const match of heuristicMatches) {
15392
16592
  signals.push({
@@ -15414,18 +16614,6 @@ var DecisionEngine = class {
15414
16614
  artifact: result.url
15415
16615
  });
15416
16616
  }
15417
- for (const flag of result.flags) {
15418
- signals.push({
15419
- decision: "ask",
15420
- category: "network_egress",
15421
- confidence: 0.75,
15422
- severity: "warning",
15423
- source: "url_check",
15424
- threatId: null,
15425
- reason: `Suspicious URL: ${flag}`,
15426
- artifact: result.url
15427
- });
15428
- }
15429
16617
  }
15430
16618
  if (packageCheckResults) {
15431
16619
  for (const pkg of packageCheckResults) {
@@ -15463,6 +16651,36 @@ var DecisionEngine = class {
15463
16651
  }
15464
16652
  }
15465
16653
  }
16654
+ if (piCheckResults) {
16655
+ const highRisk = piThresholds?.highRisk ?? DEFAULT_PI_HIGH_RISK_THRESHOLD;
16656
+ const mediumRisk = piThresholds?.mediumRisk ?? DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
16657
+ const suppressMediumPi = this.sensitivity === "relaxed";
16658
+ for (const result of piCheckResults) {
16659
+ if (result.risk >= highRisk) {
16660
+ signals.push({
16661
+ decision: "deny",
16662
+ category: "prompt_injection",
16663
+ confidence: result.risk,
16664
+ severity: "critical",
16665
+ source: "pi_check",
16666
+ threatId: "PROMPT_INJECTION",
16667
+ reason: buildPiReason("Prompt injection detected", result),
16668
+ artifact: result.contentName
16669
+ });
16670
+ } else if (!suppressMediumPi && result.risk >= mediumRisk) {
16671
+ signals.push({
16672
+ decision: "ask",
16673
+ category: "prompt_injection",
16674
+ confidence: result.risk,
16675
+ severity: "warning",
16676
+ source: "pi_check",
16677
+ threatId: "PROMPT_INJECTION",
16678
+ reason: buildPiReason("Suspicious content detected", result),
16679
+ artifact: result.contentName
16680
+ });
16681
+ }
16682
+ }
16683
+ }
15466
16684
  return signals;
15467
16685
  }
15468
16686
  packageVerdictToSignal(pkg) {
@@ -15535,27 +16753,27 @@ init_config();
15535
16753
 
15536
16754
  // ../core/dist/exceptions.js
15537
16755
  var import_node_crypto6 = require("node:crypto");
15538
- var import_node_path9 = require("node:path");
16756
+ var import_node_path13 = require("node:path");
15539
16757
  init_config();
15540
16758
  init_file_utils();
15541
16759
 
15542
16760
  // ../core/dist/trusted-domains.js
15543
- var import_promises3 = require("node:fs/promises");
15544
- var import_node_path8 = require("node:path");
16761
+ var import_promises4 = require("node:fs/promises");
16762
+ var import_node_path12 = require("node:path");
15545
16763
  var import_yaml = __toESM(require_dist(), 1);
15546
16764
  init_file_utils();
15547
16765
  init_types2();
15548
16766
  async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
15549
16767
  let files;
15550
16768
  try {
15551
- files = (await (0, import_promises3.readdir)(allowlistsDir)).filter((f) => f.endsWith(".yaml")).sort();
16769
+ files = (await (0, import_promises4.readdir)(allowlistsDir)).filter((f) => f.endsWith(".yaml")).sort();
15552
16770
  } catch {
15553
16771
  logger.debug("Allowlists directory does not exist", { path: allowlistsDir });
15554
16772
  return [];
15555
16773
  }
15556
16774
  const domains = [];
15557
16775
  for (const filename of files) {
15558
- const filePath = (0, import_node_path8.join)(allowlistsDir, filename);
16776
+ const filePath = (0, import_node_path12.join)(allowlistsDir, filename);
15559
16777
  let content;
15560
16778
  try {
15561
16779
  content = await getFileContent(filePath);
@@ -15623,8 +16841,8 @@ function computeRuleId(decision, match, pattern) {
15623
16841
  const hash = (0, import_node_crypto6.createHash)("sha256").update(`${decision}:${match}:${pattern}`).digest("hex");
15624
16842
  return hash.slice(0, 8);
15625
16843
  }
15626
- async function loadExceptions(config, logger = nullLogger) {
15627
- const path = resolvePath(config.path);
16844
+ async function loadExceptions(config2, logger = nullLogger) {
16845
+ const path = resolvePath(config2.path);
15628
16846
  let raw;
15629
16847
  try {
15630
16848
  raw = await getFileContent(path);
@@ -15707,8 +16925,8 @@ async function addException(artifact, reason, exceptionsPath, logger = nullLogge
15707
16925
  if (exceptionsPath) {
15708
16926
  excConfig = { path: exceptionsPath };
15709
16927
  } else {
15710
- const config = await Promise.resolve().then(() => (init_config(), config_exports)).then((m) => m.loadConfig(void 0, logger));
15711
- excConfig = config.exceptions;
16928
+ const config2 = await Promise.resolve().then(() => (init_config(), config_exports)).then((m) => m.loadConfig(void 0, logger));
16929
+ excConfig = config2.exceptions;
15712
16930
  }
15713
16931
  const pattern = `^${escapeRegexString(artifact.value)}$`;
15714
16932
  const rule = {
@@ -15820,7 +17038,7 @@ function matchesDomain(pattern, url) {
15820
17038
  function normalizePatternPath(pattern) {
15821
17039
  const home = getHomeDir();
15822
17040
  const expanded = pattern.startsWith("~/") || pattern === "~" ? `${home}${pattern.slice(1)}` : pattern;
15823
- return (0, import_node_path9.normalize)(expanded);
17041
+ return (0, import_node_path13.normalize)(expanded);
15824
17042
  }
15825
17043
  function matchesPath(pattern, filePath) {
15826
17044
  const hasWildcard = pattern.includes("*");
@@ -15831,7 +17049,7 @@ function matchesPath(pattern, filePath) {
15831
17049
  const normalizedPath = normalizePatternPath(filePath);
15832
17050
  if (normalizedPath === normalizedPattern)
15833
17051
  return true;
15834
- if (normalizedPath.startsWith(normalizedPattern + import_node_path9.sep))
17052
+ if (normalizedPath.startsWith(normalizedPattern + import_node_path13.sep))
15835
17053
  return true;
15836
17054
  return false;
15837
17055
  }
@@ -16132,12 +17350,12 @@ var PackageChecker = class {
16132
17350
  registryClient;
16133
17351
  fileCheckClient;
16134
17352
  logger;
16135
- constructor(config = {}, logger = nullLogger) {
17353
+ constructor(config2 = {}, logger = nullLogger) {
16136
17354
  this.logger = logger;
16137
- this.registryClient = new RegistryClient({ timeout_seconds: config.registryTimeoutSeconds }, logger);
16138
- this.fileCheckClient = config.fileCheckEnabled !== false ? new FileCheckClient({
16139
- endpoint: config.fileCheckEndpoint,
16140
- timeout_seconds: config.fileCheckTimeoutSeconds
17355
+ this.registryClient = new RegistryClient({ timeout_seconds: config2.registryTimeoutSeconds }, logger);
17356
+ this.fileCheckClient = config2.fileCheckEnabled !== false ? new FileCheckClient({
17357
+ endpoint: config2.fileCheckEndpoint,
17358
+ timeout_seconds: config2.fileCheckTimeoutSeconds
16141
17359
  }, logger) : null;
16142
17360
  }
16143
17361
  async checkPackages(packages) {
@@ -16611,7 +17829,7 @@ function extractFromRequirementsTxt(content) {
16611
17829
  }
16612
17830
 
16613
17831
  // ../core/dist/statusline.js
16614
- var import_node_path10 = require("node:path");
17832
+ var import_node_path14 = require("node:path");
16615
17833
  init_config();
16616
17834
  init_file_utils();
16617
17835
  var STATUS_PREFIX = "statusline-";
@@ -16620,7 +17838,7 @@ function sanitizeSessionId(sessionId) {
16620
17838
  return sessionId.replace(/[^a-zA-Z0-9-]/g, "_");
16621
17839
  }
16622
17840
  function statusFilePath(sessionId) {
16623
- return (0, import_node_path10.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
17841
+ return (0, import_node_path14.join)(resolvePath(SAGE_DIR), `${STATUS_PREFIX}${sanitizeSessionId(sessionId)}${STATUS_SUFFIX}`);
16624
17842
  }
16625
17843
  function emptyStatus() {
16626
17844
  return {
@@ -16653,8 +17871,8 @@ async function updateSessionStatus(sessionId, verdict) {
16653
17871
  }
16654
17872
 
16655
17873
  // ../core/dist/threat-loader.js
16656
- var import_promises4 = require("node:fs/promises");
16657
- var import_node_path11 = require("node:path");
17874
+ var import_promises5 = require("node:fs/promises");
17875
+ var import_node_path15 = require("node:path");
16658
17876
  var import_yaml2 = __toESM(require_dist(), 1);
16659
17877
  init_file_utils();
16660
17878
  init_types2();
@@ -16688,13 +17906,13 @@ async function loadThreats(threatDir, logger = nullLogger) {
16688
17906
  const threats = [];
16689
17907
  let files;
16690
17908
  try {
16691
- files = (await (0, import_promises4.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
17909
+ files = (await (0, import_promises5.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
16692
17910
  } catch {
16693
17911
  logger.warn("Threat directory does not exist or is unreadable", { path: threatDir });
16694
17912
  return threats;
16695
17913
  }
16696
17914
  for (const filename of files) {
16697
- const filePath = (0, import_node_path11.join)(threatDir, filename);
17915
+ const filePath = (0, import_node_path15.join)(threatDir, filename);
16698
17916
  let content;
16699
17917
  try {
16700
17918
  content = await getFileContent(filePath);
@@ -16761,8 +17979,61 @@ async function loadThreats(threatDir, logger = nullLogger) {
16761
17979
  return threats;
16762
17980
  }
16763
17981
 
17982
+ // ../core/dist/tool-names.js
17983
+ var CANONICAL_TOOLS = [
17984
+ "Bash",
17985
+ "WebFetch",
17986
+ "Write",
17987
+ "Edit",
17988
+ "Read",
17989
+ "Delete",
17990
+ "ApplyPatch",
17991
+ "Glob",
17992
+ "Grep",
17993
+ "List",
17994
+ "CodeSearch",
17995
+ "WebSearch",
17996
+ "Question",
17997
+ "Task",
17998
+ "ReadLines",
17999
+ "MCP",
18000
+ "Unknown"
18001
+ ];
18002
+ var CANONICAL_SET = new Set(CANONICAL_TOOLS);
18003
+ function canonicalizeToolName(map, rawName) {
18004
+ if (CANONICAL_SET.has(rawName))
18005
+ return rawName;
18006
+ return map[rawName] ?? "Unknown";
18007
+ }
18008
+
16764
18009
  // ../core/dist/evaluator.js
16765
18010
  init_types2();
18011
+ var AMSI_CONTENT_SNIPPET_MAX = 200;
18012
+ var AMSI_CONTENT_NAME_MAX = 256;
18013
+ function scrubAmsiContentName(contentName) {
18014
+ const colon = contentName.indexOf(":");
18015
+ if (colon < 0)
18016
+ return scrubHomePath(contentName);
18017
+ const head = contentName.slice(0, colon + 1);
18018
+ const tail = contentName.slice(colon + 1);
18019
+ return `${head}${scrubHomePath(tail)}`;
18020
+ }
18021
+ function buildAmsiSignal(r) {
18022
+ const detectionName = r.amsiResult >= 32768 ? "AMSI|DETECTED" : r.amsiResult >= 16384 ? "AMSI|BLOCKED_BY_ADMIN" : (
18023
+ // Defensive: callers should filter these out via `isDetected || isBlockedByAdmin`,
18024
+ // but if a non-detected/non-blocked result still reaches here we emit a
18025
+ // meaningful label rather than silently dropping the entry.
18026
+ "AMSI|UNKNOWN"
18027
+ );
18028
+ const contentName = safeTruncate(scrubAmsiContentName(r.contentName), AMSI_CONTENT_NAME_MAX);
18029
+ const snippet = r.content ? safeTruncate(scrubHomePath(r.content), AMSI_CONTENT_SNIPPET_MAX) : "";
18030
+ return {
18031
+ detection_name: detectionName,
18032
+ content_name: contentName,
18033
+ amsi_result: r.amsiResult,
18034
+ ...snippet ? { content_snippet: snippet } : {}
18035
+ };
18036
+ }
16766
18037
  function allowVerdict(source = "none") {
16767
18038
  return {
16768
18039
  decision: "allow",
@@ -16777,12 +18048,12 @@ function allowVerdict(source = "none") {
16777
18048
  }
16778
18049
  async function evaluateToolCall(request, context) {
16779
18050
  const logger = context.logger ?? nullLogger;
16780
- const config = await loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
16781
- if (request.artifacts.length === 0) {
18051
+ const config2 = await loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
18052
+ if (request.artifacts.length === 0 && !config2.pi_check.enabled) {
16782
18053
  return allowVerdict("no_artifacts");
16783
18054
  }
16784
18055
  try {
16785
- const exceptions = await loadExceptions(config.exceptions, logger);
18056
+ const exceptions = await loadExceptions(config2.exceptions, logger);
16786
18057
  const denyMatch = findDenyException(exceptions, request.artifacts);
16787
18058
  if (denyMatch) {
16788
18059
  const verdict2 = {
@@ -16798,16 +18069,33 @@ async function evaluateToolCall(request, context) {
16798
18069
  ]
16799
18070
  };
16800
18071
  try {
16801
- await logVerdict(config.logging, request.sessionId, request.toolName, request.toolInput, verdict2, false, request.conversationId, request.agentRuntime, request.hookType, void 0, void 0);
18072
+ await logVerdict(config2.logging, {
18073
+ sessionId: request.sessionId,
18074
+ toolName: request.toolName,
18075
+ toolInput: request.toolInput,
18076
+ verdict: verdict2,
18077
+ conversationId: request.conversationId,
18078
+ agentRuntime: request.agentRuntime,
18079
+ hookType: request.hookType
18080
+ });
16802
18081
  } catch {
16803
18082
  }
16804
18083
  return verdict2;
16805
18084
  }
16806
18085
  try {
16807
- const allowlist = await loadAllowlist(config.allowlist, logger);
18086
+ const allowlist = await loadAllowlist(config2.allowlist, logger);
16808
18087
  if (isAllowlisted(allowlist, request.artifacts)) {
16809
18088
  const allowV = allowVerdict("allowlisted");
16810
- await logVerdict(config.logging, request.sessionId, request.toolName, request.toolInput, allowV, true, request.conversationId, request.agentRuntime, request.hookType, void 0, void 0);
18089
+ await logVerdict(config2.logging, {
18090
+ sessionId: request.sessionId,
18091
+ toolName: request.toolName,
18092
+ toolInput: request.toolInput,
18093
+ verdict: allowV,
18094
+ userOverride: true,
18095
+ conversationId: request.conversationId,
18096
+ agentRuntime: request.agentRuntime,
18097
+ hookType: request.hookType
18098
+ });
16811
18099
  return allowV;
16812
18100
  }
16813
18101
  } catch {
@@ -16816,27 +18104,36 @@ async function evaluateToolCall(request, context) {
16816
18104
  if (allowMatch) {
16817
18105
  const allowV = allowVerdict("exception");
16818
18106
  try {
16819
- await logVerdict(config.logging, request.sessionId, request.toolName, request.toolInput, allowV, true, request.conversationId, request.agentRuntime, request.hookType, void 0, void 0);
18107
+ await logVerdict(config2.logging, {
18108
+ sessionId: request.sessionId,
18109
+ toolName: request.toolName,
18110
+ toolInput: request.toolInput,
18111
+ verdict: allowV,
18112
+ userOverride: true,
18113
+ conversationId: request.conversationId,
18114
+ agentRuntime: request.agentRuntime,
18115
+ hookType: request.hookType
18116
+ });
16820
18117
  } catch {
16821
18118
  }
16822
18119
  return allowV;
16823
18120
  }
16824
18121
  } catch {
16825
18122
  }
16826
- let cache = null;
18123
+ let cache2 = null;
16827
18124
  try {
16828
- cache = new VerdictCache(config.cache, logger, VERSION);
16829
- await cache.load();
18125
+ cache2 = new VerdictCache(config2.cache, logger, VERSION);
18126
+ await cache2.load();
16830
18127
  } catch {
16831
- cache = null;
18128
+ cache2 = null;
16832
18129
  }
16833
18130
  const urls = request.artifacts.filter((artifact) => artifact.type === "url").map((artifact) => artifact.value);
16834
- const { cachedUrlVerdicts, uncachedUrls } = partitionUrlsByCache(urls, cache);
18131
+ const { cachedUrlVerdicts, uncachedUrls } = partitionUrlsByCache(urls, cache2);
16835
18132
  let heuristicMatches = [];
16836
- if (config.heuristics_enabled) {
18133
+ if (config2.heuristics_enabled) {
16837
18134
  let threats = await loadThreats(context.threatsDir, logger);
16838
- if (config.disabled_threats.length > 0) {
16839
- const disabledSet = new Set(config.disabled_threats);
18135
+ if (config2.disabled_threats.length > 0) {
18136
+ const disabledSet = new Set(config2.disabled_threats);
16840
18137
  threats = threats.filter((t) => !disabledSet.has(t.id));
16841
18138
  }
16842
18139
  const trustedDomains = await loadTrustedDomains(context.allowlistsDir, logger);
@@ -16844,17 +18141,17 @@ async function evaluateToolCall(request, context) {
16844
18141
  heuristicMatches = heuristics.match(request.artifacts);
16845
18142
  }
16846
18143
  let urlCheckResults = [];
16847
- const urlsToCheck = cache ? uncachedUrls : urls;
16848
- if (urlsToCheck.length > 0 && config.url_check.enabled) {
18144
+ const urlsToCheck = cache2 ? uncachedUrls : urls;
18145
+ if (urlsToCheck.length > 0 && config2.url_check.enabled) {
16849
18146
  try {
16850
- const client = new UrlCheckClient(config.url_check, logger);
18147
+ const client = new UrlCheckClient(config2.url_check, logger);
16851
18148
  urlCheckResults = await client.checkUrls(urlsToCheck);
16852
18149
  } catch {
16853
18150
  }
16854
18151
  }
16855
- const packageCheckResults = await checkPackages(request, config, cache, logger);
18152
+ const packageCheckResults = await checkPackages(request, config2, cache2, logger);
16856
18153
  const amsiCheckResults = [];
16857
- if (config.amsi_check.enabled && isAmsiSupported()) {
18154
+ if (config2.amsi_check.enabled && isAmsiSupported()) {
16858
18155
  let amsiClient = null;
16859
18156
  try {
16860
18157
  amsiClient = new AmsiClient(logger);
@@ -16864,23 +18161,33 @@ async function evaluateToolCall(request, context) {
16864
18161
  if (request.toolName === "Bash") {
16865
18162
  const command = request.toolInput.command ?? "";
16866
18163
  if (command) {
16867
- scans.push({ content: command, name: `Bash:command` });
18164
+ scans.push({ scanType: "Bash", name: "command", content: command });
16868
18165
  }
16869
18166
  } else if (request.toolName === "Write") {
16870
18167
  const filePath = request.toolInput.file_path ?? "";
16871
18168
  const content = request.toolInput.content ?? "";
16872
18169
  if (content) {
16873
- scans.push({ content, name: `Write:${filePath}` });
18170
+ scans.push({ scanType: "Write", name: filePath, content });
16874
18171
  }
16875
18172
  } else if (request.toolName === "Edit") {
16876
18173
  const filePath = request.toolInput.file_path ?? "";
16877
18174
  const newString = request.toolInput.new_string ?? "";
16878
18175
  if (newString) {
16879
- scans.push({ content: newString, name: `Edit:${filePath}` });
18176
+ scans.push({ scanType: "Edit", name: filePath, content: newString });
18177
+ }
18178
+ } else if (request.toolName === "ApplyPatch") {
18179
+ const patch = request.toolInput.input ?? request.toolInput.patch ?? "";
18180
+ if (patch) {
18181
+ const target = request.artifacts.find((a) => a.type === "file_path");
18182
+ scans.push({
18183
+ scanType: "ApplyPatch",
18184
+ name: target?.value ?? "unknown",
18185
+ content: patch
18186
+ });
16880
18187
  }
16881
18188
  }
16882
18189
  for (const scan of scans) {
16883
- const result = await amsiClient.scanString(scan.content, scan.name);
18190
+ const result = await amsiClient.scanString(scan.scanType, scan.name, scan.content);
16884
18191
  if (result) {
16885
18192
  amsiCheckResults.push(result);
16886
18193
  }
@@ -16891,12 +18198,50 @@ async function evaluateToolCall(request, context) {
16891
18198
  amsiClient?.close();
16892
18199
  }
16893
18200
  }
16894
- const engine = new DecisionEngine(config.sensitivity);
18201
+ const allPiResults = [];
18202
+ const piDenySignals = [];
18203
+ const heuristicPiDeny = heuristicMatches.some((m) => m.threat.action === "block" && m.threat.category === "prompt_injection");
18204
+ const heuristicDeny = heuristicMatches.some((m) => m.threat.action === "block");
18205
+ const urlAlreadyDenied = cachedUrlVerdicts.size > 0 && [...cachedUrlVerdicts.values()].some((v) => v.verdict !== "allow");
18206
+ const urlCheckDenied = urlCheckResults.some((r) => r.isMalicious);
18207
+ if (config2.pi_check.enabled && !heuristicPiDeny && !heuristicDeny && !urlAlreadyDenied && !urlCheckDenied && request.toolName === "WebFetch") {
18208
+ try {
18209
+ const url = extractWebFetchUrl(request.toolInput);
18210
+ if (url) {
18211
+ const ext = getUrlExtension(url);
18212
+ const skip = ext != null && !SCANNABLE_EXTENSIONS.has(ext);
18213
+ if (!skip) {
18214
+ const fetcher = new ContentFetchClient(4e3, config2.pi_check.max_content_length, logger);
18215
+ const fetched = await fetcher.fetchTextContent(url);
18216
+ if (fetched) {
18217
+ const shouldScan = ext != null || isScannableContent(fetched.content);
18218
+ if (shouldScan) {
18219
+ const provider = createPiProvider(config2, context, logger);
18220
+ const result = await provider.checkContent(fetched.content, `WebFetch:${url}`);
18221
+ if (result) {
18222
+ allPiResults.push(result);
18223
+ if (result.risk >= config2.pi_check.high_risk_threshold) {
18224
+ piDenySignals.push(result);
18225
+ }
18226
+ }
18227
+ }
18228
+ }
18229
+ }
18230
+ }
18231
+ } catch {
18232
+ }
18233
+ }
18234
+ const engine = new DecisionEngine(config2.sensitivity);
16895
18235
  let verdict = await engine.decide({
16896
18236
  heuristicMatches,
16897
18237
  urlCheckResults,
16898
18238
  packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
16899
- amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0
18239
+ amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
18240
+ piCheckResults: piDenySignals.length > 0 ? piDenySignals : void 0,
18241
+ piThresholds: {
18242
+ highRisk: config2.pi_check.high_risk_threshold,
18243
+ mediumRisk: config2.pi_check.medium_risk_threshold
18244
+ }
16900
18245
  });
16901
18246
  if (cachedUrlVerdicts.size > 0 && verdict.decision === "allow") {
16902
18247
  for (const [url, cachedVerdict] of cachedUrlVerdicts) {
@@ -16916,7 +18261,7 @@ async function evaluateToolCall(request, context) {
16916
18261
  break;
16917
18262
  }
16918
18263
  }
16919
- await cacheUrlResults(urlCheckResults, cache);
18264
+ await cacheUrlResults(urlCheckResults, cache2);
16920
18265
  function formatPackageDetectionName(p) {
16921
18266
  const base = `PKG|${p.verdict}|registry=${p.registry}|name=${p.packageName}`;
16922
18267
  if (p.verdict === "suspicious_age") {
@@ -16937,7 +18282,7 @@ async function evaluateToolCall(request, context) {
16937
18282
  }));
16938
18283
  }
16939
18284
  if (urlCheckResults.length > 0) {
16940
- const relevant = urlCheckResults.filter((r) => r.isMalicious || r.flags.length > 0);
18285
+ const relevant = urlCheckResults.filter((r) => r.isMalicious);
16941
18286
  if (relevant.length > 0) {
16942
18287
  auditSignals.url_checks = relevant.flatMap((r) => {
16943
18288
  return r.detections.map((d) => ({
@@ -16947,6 +18292,20 @@ async function evaluateToolCall(request, context) {
16947
18292
  });
16948
18293
  }
16949
18294
  }
18295
+ if (cachedUrlVerdicts.size > 0) {
18296
+ for (const [url, cachedEntry] of cachedUrlVerdicts) {
18297
+ if (cachedEntry.verdict !== "deny")
18298
+ continue;
18299
+ const labels = cachedEntry.urlSignalLabels ?? [];
18300
+ if (labels.length === 0)
18301
+ continue;
18302
+ auditSignals.url_checks ??= [];
18303
+ auditSignals.url_checks.push(...labels.map((label) => ({
18304
+ detection_name: label,
18305
+ url
18306
+ })));
18307
+ }
18308
+ }
16950
18309
  if (packageCheckResults.length > 0) {
16951
18310
  const relevant = packageCheckResults.filter((p) => p.verdict !== "clean");
16952
18311
  if (relevant.length > 0) {
@@ -16965,10 +18324,39 @@ async function evaluateToolCall(request, context) {
16965
18324
  }));
16966
18325
  }
16967
18326
  }
18327
+ if (allPiResults.length > 0) {
18328
+ const piSnippetFloor = config2.pi_check.medium_risk_threshold;
18329
+ auditSignals.pi_checks = allPiResults.map((r) => ({
18330
+ risk: r.risk,
18331
+ model_id: r.modelId,
18332
+ content_name: r.contentName,
18333
+ ...r.contentSnippet && r.risk >= piSnippetFloor ? { content_snippet: scrubHomePath(r.contentSnippet) } : {}
18334
+ }));
18335
+ }
18336
+ if (amsiCheckResults.length > 0) {
18337
+ const relevantAmsi = amsiCheckResults.filter((r) => r.isDetected || r.isBlockedByAdmin);
18338
+ if (relevantAmsi.length > 0) {
18339
+ auditSignals.amsi_checks = relevantAmsi.map((r) => buildAmsiSignal(r));
18340
+ }
18341
+ }
16968
18342
  const eventId = (0, import_node_crypto7.randomUUID)();
16969
18343
  const resolvedSignals = Object.keys(auditSignals).length > 0 ? auditSignals : void 0;
18344
+ const builtContent = buildContentSnapshot(request.toolName, request.toolInput, request.artifacts, auditSignals);
18345
+ const resolvedContent = Object.keys(builtContent).length > 0 ? builtContent : void 0;
16970
18346
  try {
16971
- await logVerdict(config.logging, request.sessionId, request.toolName, request.toolInput, verdict, false, request.conversationId, request.agentRuntime, request.hookType, resolvedSignals, eventId);
18347
+ await logVerdict(config2.logging, {
18348
+ sessionId: request.sessionId,
18349
+ toolName: request.toolName,
18350
+ toolInput: request.toolInput,
18351
+ verdict,
18352
+ conversationId: request.conversationId,
18353
+ agentRuntime: request.agentRuntime,
18354
+ hookType: request.hookType,
18355
+ signals: resolvedSignals,
18356
+ content: resolvedContent,
18357
+ eventId,
18358
+ toolUseId: request.toolUseId
18359
+ });
16972
18360
  } catch {
16973
18361
  }
16974
18362
  if (verdict.decision === "deny") {
@@ -16976,11 +18364,12 @@ async function evaluateToolCall(request, context) {
16976
18364
  await sendCommunityIqDetection({
16977
18365
  eventId,
16978
18366
  agentRuntime: request.agentRuntime,
18367
+ agentRuntimeVersion: request.agentRuntimeVersion,
16979
18368
  hookType: request.hookType,
16980
18369
  toolName: request.toolName,
16981
- toolInput: request.toolInput,
18370
+ content: resolvedContent,
16982
18371
  signals: resolvedSignals,
16983
- communityIqEnabled: config.community_iq,
18372
+ communityIqEnabled: config2.community_iq,
16984
18373
  logger
16985
18374
  });
16986
18375
  } catch {
@@ -16992,15 +18381,18 @@ async function evaluateToolCall(request, context) {
16992
18381
  } catch {
16993
18382
  }
16994
18383
  }
18384
+ const piWarnings = allPiResults.filter((r) => r.risk >= config2.pi_check.medium_risk_threshold && r.risk < config2.pi_check.high_risk_threshold);
18385
+ if (piWarnings.length > 0 && config2.sensitivity !== "relaxed")
18386
+ verdict.piWarnings = piWarnings;
16995
18387
  return verdict;
16996
18388
  }
16997
- function partitionUrlsByCache(urls, cache) {
18389
+ function partitionUrlsByCache(urls, cache2) {
16998
18390
  const cachedUrlVerdicts = /* @__PURE__ */ new Map();
16999
18391
  let uncachedUrls = [];
17000
- if (cache && urls.length > 0) {
18392
+ if (cache2 && urls.length > 0) {
17001
18393
  try {
17002
18394
  for (const url of urls) {
17003
- const cached = cache.getUrl(url);
18395
+ const cached = cache2.getUrl(url);
17004
18396
  if (cached !== null) {
17005
18397
  cachedUrlVerdicts.set(url, cached);
17006
18398
  } else {
@@ -17013,8 +18405,8 @@ function partitionUrlsByCache(urls, cache) {
17013
18405
  }
17014
18406
  return { cachedUrlVerdicts, uncachedUrls };
17015
18407
  }
17016
- async function cacheUrlResults(urlCheckResults, cache) {
17017
- if (!cache)
18408
+ async function cacheUrlResults(urlCheckResults, cache2) {
18409
+ if (!cache2)
17018
18410
  return;
17019
18411
  try {
17020
18412
  for (const result of urlCheckResults) {
@@ -17026,14 +18418,10 @@ async function cacheUrlResults(urlCheckResults, cache) {
17026
18418
  reasons: [
17027
18419
  `URL check: malicious (${result.findings.map((finding) => `${finding.severityName}/${finding.typeName}`).join(", ")})`
17028
18420
  ],
17029
- source: "url_check"
17030
- };
17031
- } else if (result.flags.length > 0) {
17032
- cachedVerdict = {
17033
- verdict: "ask",
17034
- severity: "warning",
17035
- reasons: [`URL check: suspicious (${result.flags.join(", ")})`],
17036
- source: "url_check"
18421
+ source: "url_check",
18422
+ // Always populated for malicious URLs (possibly empty array). Locked in by
18423
+ // evaluator.test.ts so a future refactor cannot silently drop the labels.
18424
+ urlSignalLabels: result.detections
17037
18425
  };
17038
18426
  } else {
17039
18427
  cachedVerdict = {
@@ -17043,15 +18431,15 @@ async function cacheUrlResults(urlCheckResults, cache) {
17043
18431
  source: "url_check"
17044
18432
  };
17045
18433
  }
17046
- cache.putUrl(result.url, cachedVerdict, result.isMalicious);
18434
+ cache2.putUrl(result.url, cachedVerdict, result.isMalicious);
17047
18435
  }
17048
- await cache.save();
18436
+ await cache2.save();
17049
18437
  } catch {
17050
18438
  }
17051
18439
  }
17052
- async function checkPackages(request, config, cache, logger) {
18440
+ async function checkPackages(request, config2, cache2, logger) {
17053
18441
  const results = [];
17054
- if (!config.package_check.enabled)
18442
+ if (!config2.package_check.enabled)
17055
18443
  return results;
17056
18444
  try {
17057
18445
  let parsedPackages;
@@ -17068,7 +18456,7 @@ async function checkPackages(request, config, cache, logger) {
17068
18456
  const uncached = [];
17069
18457
  for (const pkg of parsedPackages) {
17070
18458
  const cacheKey2 = `${pkg.registry}:${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`;
17071
- const cached = cache?.getPackage(cacheKey2);
18459
+ const cached = cache2?.getPackage(cacheKey2);
17072
18460
  if (cached && cached.verdict !== "allow") {
17073
18461
  results.push({
17074
18462
  packageName: pkg.name,
@@ -17083,19 +18471,19 @@ async function checkPackages(request, config, cache, logger) {
17083
18471
  }
17084
18472
  if (uncached.length > 0) {
17085
18473
  const checker = new PackageChecker({
17086
- registryTimeoutSeconds: config.package_check.timeout_seconds,
17087
- fileCheckEndpoint: config.file_check.endpoint,
17088
- fileCheckTimeoutSeconds: config.file_check.timeout_seconds,
17089
- fileCheckEnabled: config.file_check.enabled
18474
+ registryTimeoutSeconds: config2.package_check.timeout_seconds,
18475
+ fileCheckEndpoint: config2.file_check.endpoint,
18476
+ fileCheckTimeoutSeconds: config2.file_check.timeout_seconds,
18477
+ fileCheckEnabled: config2.file_check.enabled
17090
18478
  }, logger);
17091
18479
  const checked = await checker.checkPackages(uncached);
17092
18480
  results.push(...checked);
17093
- if (cache) {
18481
+ if (cache2) {
17094
18482
  for (const result of checked) {
17095
18483
  const pkg = uncached.find((p) => p.name === result.packageName);
17096
18484
  const cacheKey2 = `${result.registry}:${result.packageName}${pkg?.version ? `@${pkg.version}` : ""}`;
17097
18485
  const isCritical = result.verdict === "malicious" || result.verdict === "not_found";
17098
- cache.putPackage(cacheKey2, {
18486
+ cache2.putPackage(cacheKey2, {
17099
18487
  verdict: result.verdict === "clean" ? "allow" : isCritical ? "deny" : "ask",
17100
18488
  severity: isCritical ? "critical" : "warning",
17101
18489
  reasons: [result.details],
@@ -17108,6 +18496,14 @@ async function checkPackages(request, config, cache, logger) {
17108
18496
  }
17109
18497
  return results;
17110
18498
  }
18499
+ function createPiProvider(config2, _context, logger) {
18500
+ return new BundledPiProvider({
18501
+ modelPath: config2.pi_check.model_path,
18502
+ maxContentLength: config2.pi_check.max_content_length,
18503
+ mediumRiskThreshold: config2.pi_check.medium_risk_threshold,
18504
+ logger
18505
+ });
18506
+ }
17111
18507
 
17112
18508
  // ../core/dist/index.js
17113
18509
  init_file_utils();
@@ -17133,15 +18529,35 @@ function formatUpdateNotice(result) {
17133
18529
  return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/gendigitalinc/sage)`;
17134
18530
  }
17135
18531
  function formatStartupClean(version, versionCheck, branding2 = defaultBranding) {
17136
- const base = `\u{1F6E1}\uFE0F ${branding2.banner_text} v${version} \u2705 No threats found`;
18532
+ const base = `\u{1F6E1}\uFE0F ${branding2.name} v${version} \u2705 No threats found`;
17137
18533
  if (versionCheck?.updateAvailable) {
17138
18534
  return `${base}
17139
18535
  ${formatUpdateNotice(versionCheck)}`;
17140
18536
  }
17141
18537
  return base;
17142
18538
  }
17143
- function formatThreatBanner(version, results, versionCheck, branding2 = defaultBranding) {
17144
- const header = `\u{1F6E1}\uFE0F ${branding2.banner_text} v${version} \u2014 Threat Detected`;
18539
+ function totalHighCrit(results) {
18540
+ let n = 0;
18541
+ for (const r of results) {
18542
+ for (const f of r.findings) {
18543
+ if (f.severity === "critical" || f.severity === "high")
18544
+ n++;
18545
+ }
18546
+ }
18547
+ return n;
18548
+ }
18549
+ function findFirstHighCrit(results) {
18550
+ for (const r of results) {
18551
+ for (const f of r.findings) {
18552
+ if (f.severity === "critical" || f.severity === "high") {
18553
+ return { plugin: r.plugin, finding: f };
18554
+ }
18555
+ }
18556
+ }
18557
+ return null;
18558
+ }
18559
+ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
18560
+ const header = `\u{1F6E1}\uFE0F ${branding2.name} v${version} \u2014 Threat Detected`;
17145
18561
  const lines = [" ", header, separatorLine(SEPARATOR_WIDTH)];
17146
18562
  const MAX_FINDINGS = 5;
17147
18563
  let first = true;
@@ -17163,6 +18579,12 @@ function formatThreatBanner(version, results, versionCheck, branding2 = defaultB
17163
18579
  lines.push(kv("Source", f.sourceFile));
17164
18580
  const actionLabel = f.action === "block" ? "Blocked" : "Flagged";
17165
18581
  lines.push(kv("Action", actionLabel));
18582
+ if (f.recommendations && f.recommendations.length > 0) {
18583
+ lines.push(kv("Recommendations", ""));
18584
+ for (const rec of f.recommendations) {
18585
+ lines.push(` ${" ".repeat(PAD)}- ${rec}`);
18586
+ }
18587
+ }
17166
18588
  }
17167
18589
  const overflow = highCrit.length - MAX_FINDINGS;
17168
18590
  if (overflow > 0) {
@@ -17176,9 +18598,36 @@ function formatThreatBanner(version, results, versionCheck, branding2 = defaultB
17176
18598
  }
17177
18599
  return lines.join("\n");
17178
18600
  }
17179
- function formatSessionStartMessage(version, result, branding2 = defaultBranding) {
18601
+ function formatThreatBannerCompact(version, results, versionCheck, branding2) {
18602
+ const total = totalHighCrit(results);
18603
+ const first = findFirstHighCrit(results);
18604
+ if (!first || total === 0) {
18605
+ return formatStartupClean(version, versionCheck, branding2);
18606
+ }
18607
+ const emoji = severityEmoji(first.finding.severity);
18608
+ const lines = [
18609
+ `\u{1F6E1}\uFE0F ${branding2.name} v${version} \u2014 Threat Detected in ${first.plugin.key}`,
18610
+ `${emoji} ${first.finding.title} (${first.finding.severity.toUpperCase()})`,
18611
+ first.finding.sourceFile
18612
+ ];
18613
+ const remaining = total - 1;
18614
+ if (remaining > 0) {
18615
+ lines.push(`\u2026and ${remaining} more finding${remaining === 1 ? "" : "s"}`);
18616
+ }
18617
+ if (versionCheck?.updateAvailable) {
18618
+ lines.push(formatUpdateNotice(versionCheck));
18619
+ }
18620
+ return lines.join("\n");
18621
+ }
18622
+ function formatThreatBanner(version, results, versionCheck, branding2 = defaultBranding, style = "verbose") {
18623
+ if (style === "compact") {
18624
+ return formatThreatBannerCompact(version, results, versionCheck, branding2);
18625
+ }
18626
+ return formatThreatBannerVerbose(version, results, versionCheck, branding2);
18627
+ }
18628
+ function formatSessionStartMessage(version, result, branding2 = defaultBranding, style = "verbose") {
17180
18629
  if (result.scanResults.length > 0) {
17181
- return formatThreatBanner(version, result.scanResults, result.versionCheck, branding2);
18630
+ return formatThreatBanner(version, result.scanResults, result.versionCheck, branding2, style);
17182
18631
  }
17183
18632
  return formatStartupClean(version, result.versionCheck, branding2);
17184
18633
  }
@@ -17196,8 +18645,8 @@ async function guardToolCall(request, context, approvalStore) {
17196
18645
  return { verdict };
17197
18646
  }
17198
18647
  const logger = context.logger ?? nullLogger;
17199
- const config = await loadConfig(context.configPath, logger);
17200
- if (config.sensitivity === "paranoid") {
18648
+ const config2 = await loadConfig(context.configPath, logger);
18649
+ if (config2.sensitivity === "paranoid") {
17201
18650
  return { verdict: { ...verdict, decision: "deny" } };
17202
18651
  }
17203
18652
  approvalStore.setPending(actionId, {
@@ -17221,7 +18670,7 @@ function summarizeArtifacts(artifacts) {
17221
18670
  function formatDenyMessage(verdict, branding2 = defaultBranding) {
17222
18671
  const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 5).join("; ") : verdict.category;
17223
18672
  return [
17224
- `${branding2.product_name} blocked this action.`,
18673
+ `${branding2.name} blocked this action.`,
17225
18674
  `Severity: ${verdict.severity}`,
17226
18675
  `Category: ${verdict.category}`,
17227
18676
  `Reason: ${reasons}`
@@ -17231,13 +18680,16 @@ function formatDenyMessage(verdict, branding2 = defaultBranding) {
17231
18680
  // ../core/dist/marketplace-migration.js
17232
18681
  init_config();
17233
18682
 
18683
+ // ../core/dist/model-download.js
18684
+ init_types2();
18685
+
17234
18686
  // ../core/dist/plugin-scan-cache.js
17235
18687
  var import_node_crypto8 = require("node:crypto");
17236
- var import_promises5 = require("node:fs/promises");
17237
- var import_node_path12 = require("node:path");
18688
+ var import_promises6 = require("node:fs/promises");
18689
+ var import_node_path16 = require("node:path");
17238
18690
  init_file_utils();
17239
18691
  init_types2();
17240
- var DEFAULT_CACHE_PATH = (0, import_node_path12.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
18692
+ var DEFAULT_CACHE_PATH = (0, import_node_path16.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
17241
18693
  var CACHE_TTL_DAYS = 7;
17242
18694
  function cacheKey(pluginKey, version, lastUpdated) {
17243
18695
  return `${pluginKey}:${version}:${lastUpdated}`;
@@ -17249,13 +18701,13 @@ async function computeConfigHash(sageVersion, ...dirs) {
17249
18701
  for (const dir of dirs) {
17250
18702
  let files;
17251
18703
  try {
17252
- files = (await (0, import_promises5.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
18704
+ files = (await (0, import_promises6.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
17253
18705
  } catch {
17254
18706
  continue;
17255
18707
  }
17256
18708
  for (const file of files) {
17257
18709
  try {
17258
- const content = await getFileContent((0, import_node_path12.join)(dir, file));
18710
+ const content = await getFileContent((0, import_node_path16.join)(dir, file));
17259
18711
  h.update(content);
17260
18712
  } catch {
17261
18713
  }
@@ -17296,11 +18748,11 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
17296
18748
  return { configHash, entries: {} };
17297
18749
  }
17298
18750
  }
17299
- async function saveScanCache(cache, cachePath = DEFAULT_CACHE_PATH, logger = nullLogger) {
18751
+ async function saveScanCache(cache2, cachePath = DEFAULT_CACHE_PATH, logger = nullLogger) {
17300
18752
  try {
17301
18753
  const data = {
17302
- config_hash: cache.configHash,
17303
- entries: Object.fromEntries(Object.entries(cache.entries).map(([key, entry]) => [
18754
+ config_hash: cache2.configHash,
18755
+ entries: Object.fromEntries(Object.entries(cache2.entries).map(([key, entry]) => [
17304
18756
  key,
17305
18757
  {
17306
18758
  plugin_key: entry.pluginKey,
@@ -17315,9 +18767,9 @@ async function saveScanCache(cache, cachePath = DEFAULT_CACHE_PATH, logger = nul
17315
18767
  logger.warn(`Failed to save scan cache to ${cachePath}`, { error: String(e) });
17316
18768
  }
17317
18769
  }
17318
- function isCached(cache, pluginKey, version, lastUpdated) {
18770
+ function isCached(cache2, pluginKey, version, lastUpdated) {
17319
18771
  const key = cacheKey(pluginKey, version, lastUpdated);
17320
- const entry = cache.entries[key];
18772
+ const entry = cache2.entries[key];
17321
18773
  if (!entry)
17322
18774
  return false;
17323
18775
  try {
@@ -17328,15 +18780,15 @@ function isCached(cache, pluginKey, version, lastUpdated) {
17328
18780
  return false;
17329
18781
  }
17330
18782
  }
17331
- function getCached(cache, pluginKey, version, lastUpdated) {
17332
- if (!isCached(cache, pluginKey, version, lastUpdated))
18783
+ function getCached(cache2, pluginKey, version, lastUpdated) {
18784
+ if (!isCached(cache2, pluginKey, version, lastUpdated))
17333
18785
  return null;
17334
18786
  const key = cacheKey(pluginKey, version, lastUpdated);
17335
- return cache.entries[key] ?? null;
18787
+ return cache2.entries[key] ?? null;
17336
18788
  }
17337
- function storeResult(cache, pluginKey, version, lastUpdated, findings) {
18789
+ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
17338
18790
  const key = cacheKey(pluginKey, version, lastUpdated);
17339
- cache.entries[key] = {
18791
+ cache2.entries[key] = {
17340
18792
  pluginKey,
17341
18793
  version,
17342
18794
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17345,13 +18797,179 @@ function storeResult(cache, pluginKey, version, lastUpdated, findings) {
17345
18797
  }
17346
18798
 
17347
18799
  // ../core/dist/plugin-scanner.js
17348
- var import_node_crypto9 = require("node:crypto");
17349
- var import_promises6 = require("node:fs/promises");
17350
- var import_node_path13 = require("node:path");
18800
+ var import_node_crypto10 = require("node:crypto");
18801
+ var import_promises8 = require("node:fs/promises");
18802
+ var import_node_path18 = require("node:path");
17351
18803
  init_file_utils();
18804
+
18805
+ // ../core/dist/skill-id.js
18806
+ var import_node_crypto9 = require("node:crypto");
18807
+ var import_promises7 = require("node:fs/promises");
18808
+ var import_node_path17 = require("node:path");
18809
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
18810
+ function isContained(childReal, rootReal) {
18811
+ return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path17.sep}`);
18812
+ }
18813
+ function normalizePath(p) {
18814
+ let out = p.replace(/\\/g, "/").normalize("NFC");
18815
+ out = out.split("/").filter((c) => c !== "" && c !== ".").join("/");
18816
+ while (out.startsWith("./") || out.startsWith("/")) {
18817
+ out = out.startsWith("./") ? out.slice(2) : out.slice(1);
18818
+ }
18819
+ return out;
18820
+ }
18821
+ async function entriesFromDirectory(dirPath) {
18822
+ const absDir = (0, import_node_path17.resolve)(dirPath);
18823
+ const rootReal = await (0, import_promises7.realpath)(absDir);
18824
+ const entries = [];
18825
+ const visited = /* @__PURE__ */ new Set();
18826
+ async function walk(currentPath) {
18827
+ const real = await (0, import_promises7.realpath)(currentPath);
18828
+ if (visited.has(real))
18829
+ return;
18830
+ if (!isContained(real, rootReal))
18831
+ return;
18832
+ visited.add(real);
18833
+ const items = await (0, import_promises7.readdir)(currentPath);
18834
+ const dirs = [];
18835
+ const files = [];
18836
+ for (const item of items) {
18837
+ const fullPath = (0, import_node_path17.join)(currentPath, item);
18838
+ const st = await (0, import_promises7.stat)(fullPath);
18839
+ if (st.isDirectory())
18840
+ dirs.push(item);
18841
+ else if (st.isFile())
18842
+ files.push(item);
18843
+ }
18844
+ dirs.sort();
18845
+ files.sort();
18846
+ for (const d of dirs) {
18847
+ const fullPath = (0, import_node_path17.join)(currentPath, d);
18848
+ const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
18849
+ entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
18850
+ await walk(fullPath);
18851
+ }
18852
+ for (const f of files) {
18853
+ const fullPath = (0, import_node_path17.join)(currentPath, f);
18854
+ const fileReal = await (0, import_promises7.realpath)(fullPath);
18855
+ if (!isContained(fileReal, rootReal))
18856
+ continue;
18857
+ const relPath = (0, import_node_path17.relative)(absDir, fullPath).replace(/\\/g, "/");
18858
+ entries.push({
18859
+ entryPath: relPath,
18860
+ isDir: false,
18861
+ content: await (0, import_promises7.readFile)(fullPath)
18862
+ });
18863
+ }
18864
+ }
18865
+ await walk(absDir);
18866
+ return entries;
18867
+ }
18868
+ function computeSkillId(entries) {
18869
+ let normalized = entries.map((e) => ({ ...e, entryPath: normalizePath(e.entryPath) })).filter((e) => e.entryPath !== "");
18870
+ if (normalized.length > 0) {
18871
+ const topLevel = new Set(normalized.map((e) => e.entryPath.split("/")[0]));
18872
+ if (topLevel.size === 1) {
18873
+ const prefix = `${[...topLevel][0]}/`;
18874
+ normalized = normalized.map((e) => ({
18875
+ ...e,
18876
+ entryPath: e.entryPath.startsWith(prefix) ? e.entryPath.slice(prefix.length) : e.entryPath
18877
+ })).filter((e) => e.entryPath !== "");
18878
+ }
18879
+ }
18880
+ normalized = normalized.filter((e) => e.entryPath !== "skill.sig");
18881
+ const fileHashes = {};
18882
+ const treeEntries = [];
18883
+ for (const e of normalized) {
18884
+ const contentHash = (0, import_node_crypto9.createHash)("sha256").update(e.isDir ? Buffer.alloc(0) : e.content).digest("hex");
18885
+ const entryType = e.isDir ? "dir" : "file";
18886
+ treeEntries.push({ type: entryType, entryPath: e.entryPath, hash: contentHash });
18887
+ if (!e.isDir)
18888
+ fileHashes[e.entryPath] = contentHash;
18889
+ }
18890
+ treeEntries.sort((a, b) => a.entryPath < b.entryPath ? -1 : a.entryPath > b.entryPath ? 1 : 0);
18891
+ const skillHasher = (0, import_node_crypto9.createHash)("sha256");
18892
+ for (const e of treeEntries) {
18893
+ skillHasher.update(Buffer.from(`${e.type}\0${e.entryPath}\0${e.hash}
18894
+ `, "utf8"));
18895
+ }
18896
+ return { skillId: skillHasher.digest("hex"), fileHashes };
18897
+ }
18898
+ async function findSkillPackages(rootDir) {
18899
+ const found = [];
18900
+ let rootReal;
18901
+ try {
18902
+ const rootStat = await (0, import_promises7.stat)(rootDir);
18903
+ if (!rootStat.isDirectory())
18904
+ return found;
18905
+ rootReal = await (0, import_promises7.realpath)((0, import_node_path17.resolve)(rootDir));
18906
+ } catch {
18907
+ return found;
18908
+ }
18909
+ const visited = /* @__PURE__ */ new Set();
18910
+ async function walk(dir) {
18911
+ let real;
18912
+ try {
18913
+ real = await (0, import_promises7.realpath)(dir);
18914
+ } catch {
18915
+ return;
18916
+ }
18917
+ if (visited.has(real))
18918
+ return;
18919
+ if (!isContained(real, rootReal))
18920
+ return;
18921
+ visited.add(real);
18922
+ let items;
18923
+ try {
18924
+ items = await (0, import_promises7.readdir)(dir);
18925
+ } catch {
18926
+ return;
18927
+ }
18928
+ items.sort();
18929
+ if (items.includes("SKILL.md")) {
18930
+ try {
18931
+ const st = await (0, import_promises7.stat)((0, import_node_path17.join)(dir, "SKILL.md"));
18932
+ if (st.isFile())
18933
+ found.push(dir);
18934
+ } catch {
18935
+ }
18936
+ }
18937
+ for (const item of items) {
18938
+ if (SKIP_DIRS.has(item))
18939
+ continue;
18940
+ const fullPath = (0, import_node_path17.join)(dir, item);
18941
+ let st;
18942
+ try {
18943
+ st = await (0, import_promises7.stat)(fullPath);
18944
+ } catch {
18945
+ continue;
18946
+ }
18947
+ if (st.isDirectory()) {
18948
+ await walk(fullPath);
18949
+ }
18950
+ }
18951
+ }
18952
+ await walk((0, import_node_path17.resolve)(rootDir));
18953
+ return found;
18954
+ }
18955
+ async function computeSkillIdsForRoot(rootDir) {
18956
+ const folders = await findSkillPackages(rootDir);
18957
+ const out = [];
18958
+ for (const folder of folders) {
18959
+ try {
18960
+ const entries = await entriesFromDirectory(folder);
18961
+ const { skillId } = computeSkillId(entries);
18962
+ out.push({ folder, skillId });
18963
+ } catch {
18964
+ }
18965
+ }
18966
+ return out;
18967
+ }
18968
+
18969
+ // ../core/dist/plugin-scanner.js
17352
18970
  init_types2();
17353
- var DEFAULT_PLUGINS_REGISTRY = (0, import_node_path13.join)(getHomeDir(), ".claude", "plugins", "installed_plugins.json");
17354
- var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
18971
+ var DEFAULT_PLUGINS_REGISTRY = (0, import_node_path18.join)(getHomeDir(), ".claude", "plugins", "installed_plugins.json");
18972
+ var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
17355
18973
  ".py",
17356
18974
  ".js",
17357
18975
  ".ts",
@@ -17370,19 +18988,18 @@ var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
17370
18988
  ".ini",
17371
18989
  ".conf"
17372
18990
  ]);
17373
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
17374
18991
  var MAX_FILE_SIZE = 512 * 1024;
17375
18992
  async function walkPluginFiles(installPath, logger) {
17376
18993
  const files = [];
17377
18994
  async function walk(dirOrFile) {
17378
18995
  let stats;
17379
18996
  try {
17380
- stats = await (0, import_promises6.stat)(dirOrFile);
18997
+ stats = await (0, import_promises8.stat)(dirOrFile);
17381
18998
  } catch {
17382
18999
  return;
17383
19000
  }
17384
19001
  if (stats.isFile()) {
17385
- if (SCANNABLE_EXTENSIONS.has((0, import_node_path13.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
19002
+ if (SCANNABLE_EXTENSIONS2.has((0, import_node_path18.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
17386
19003
  files.push(dirOrFile);
17387
19004
  }
17388
19005
  return;
@@ -17390,14 +19007,14 @@ async function walkPluginFiles(installPath, logger) {
17390
19007
  if (stats.isDirectory()) {
17391
19008
  let entries;
17392
19009
  try {
17393
- entries = await (0, import_promises6.readdir)(dirOrFile);
19010
+ entries = await (0, import_promises8.readdir)(dirOrFile);
17394
19011
  } catch {
17395
19012
  return;
17396
19013
  }
17397
19014
  for (const entry of entries) {
17398
19015
  if (SKIP_DIRS.has(entry))
17399
19016
  continue;
17400
- const fullPath = (0, import_node_path13.join)(dirOrFile, entry);
19017
+ const fullPath = (0, import_node_path18.join)(dirOrFile, entry);
17401
19018
  await walk(fullPath);
17402
19019
  }
17403
19020
  }
@@ -17409,122 +19026,15 @@ async function walkPluginFiles(installPath, logger) {
17409
19026
  }
17410
19027
  return files;
17411
19028
  }
17412
- function isHarmlessEcho(line) {
17413
- if (!/^(echo|printf)\b/.test(line))
17414
- return false;
17415
- const withoutQuotes = line.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, "");
17416
- return !withoutQuotes.includes("|");
17417
- }
17418
- var JS_TS_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts", ".mjs", ".mts"]);
17419
- var STRING_OR_COMMENT_RE = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`|\/\/.*$|\/\*[\s\S]*?\*\//gm;
17420
- function stripJsComments(src) {
17421
- return src.replace(STRING_OR_COMMENT_RE, (match) => {
17422
- if (match.startsWith("//") || match.startsWith("/*"))
17423
- return "";
17424
- return match;
17425
- });
17426
- }
17427
- var STR_ARG = `(?:"((?:[^"\\\\]|\\\\.)*)"|'((?:[^'\\\\]|\\\\.)*)'|\`([^\`]*)\`)`;
17428
- var JS_EXEC_RE = new RegExp(`\\bexec(?:File)?(?:Sync)?\\s*\\(\\s*${STR_ARG}`, "g");
17429
- var JS_SPAWN_RE = new RegExp(`\\bspawn(?:Sync)?\\s*\\(\\s*${STR_ARG}`, "g");
17430
- var JS_SPAWN_ARGS_RE = /\bspawn(?:Sync)?\s*\(\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|`([^`]*)`)\s*,\s*\[([^\]]*)\]/g;
17431
- var SHELL_EXECUTABLES = /* @__PURE__ */ new Set([
17432
- "bash",
17433
- "sh",
17434
- "zsh",
17435
- "dash",
17436
- "ksh",
17437
- "csh",
17438
- "fish",
17439
- "cmd",
17440
- "cmd.exe",
17441
- "powershell",
17442
- "powershell.exe",
17443
- "pwsh"
17444
- ]);
17445
- function isShellExecutable(exe) {
17446
- const basename = exe.split("/").pop()?.split("\\").pop() ?? exe;
17447
- return SHELL_EXECUTABLES.has(basename);
17448
- }
17449
- var JS_EXECA_RE = new RegExp(`\\bexeca\\s*\\(\\s*${STR_ARG}`, "g");
17450
- var JS_EXECA_ARGS_RE = /\bexeca\s*\(\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|`([^`]*)`)\s*,\s*\[([^\]]*)\]/g;
17451
- var JS_BUN_SHELL_RE = new RegExp(`\\bBun\\.shell\\s*\\(\\s*${STR_ARG}`, "g");
17452
- var JS_BUN_DOLLAR_RE = /\bBun\.\$`([^`]+)`/g;
17453
- var JS_ZX_RE = /(?<!\w)\$`([^`]+)`/g;
17454
- function extractCommandsFromJsTs(content, fileName) {
17455
- const commands = /* @__PURE__ */ new Set();
17456
- const stripped = stripJsComments(content);
17457
- const collapsed = stripped.replace(/\n/g, " ");
17458
- function addMatch(m) {
17459
- const val = m[1] ?? m[2] ?? m[3];
17460
- if (val)
17461
- commands.add(val.trim());
17462
- }
17463
- for (const re of [JS_EXEC_RE, JS_SPAWN_RE, JS_EXECA_RE, JS_BUN_SHELL_RE]) {
17464
- for (const m of collapsed.matchAll(re)) {
17465
- addMatch(m);
17466
- }
17467
- }
17468
- for (const re of [JS_BUN_DOLLAR_RE, JS_ZX_RE]) {
17469
- for (const m of collapsed.matchAll(re)) {
17470
- if (m[1])
17471
- commands.add(m[1].trim());
17472
- }
17473
- }
17474
- for (const re of [JS_SPAWN_ARGS_RE, JS_EXECA_ARGS_RE]) {
17475
- for (const arrMatch of collapsed.matchAll(re)) {
17476
- const exe = (arrMatch[1] ?? arrMatch[2] ?? arrMatch[3] ?? "").trim();
17477
- if (!isShellExecutable(exe))
17478
- continue;
17479
- const arrayContent = arrMatch[4] ?? "";
17480
- const strLitRe = new RegExp(STR_ARG, "g");
17481
- for (const strMatch of arrayContent.matchAll(strLitRe)) {
17482
- const val = strMatch[1] ?? strMatch[2] ?? strMatch[3];
17483
- if (val)
17484
- commands.add(val.trim());
17485
- }
17486
- }
17487
- }
17488
- return [...commands].map((value) => ({
17489
- type: "command",
17490
- value,
17491
- context: `plugin_file:${fileName}`
17492
- }));
17493
- }
17494
- function extractArtifactsFromFile(filePath, content) {
17495
- const artifacts = [];
17496
- const fileName = filePath.split("/").pop() ?? filePath;
17497
- for (const url of extractUrls(content)) {
17498
- if (url.includes("://127.0.0.1") || url.includes("://localhost"))
17499
- continue;
17500
- artifacts.push({ type: "url", value: url, context: `plugin_file:${fileName}` });
17501
- }
17502
- const ext = (0, import_node_path13.extname)(filePath).toLowerCase();
17503
- if ([".sh", ".bash", ".zsh", ".py"].includes(ext)) {
17504
- for (const line of content.split("\n")) {
17505
- const trimmed = line.trim();
17506
- if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//") && !isHarmlessEcho(trimmed)) {
17507
- artifacts.push({
17508
- type: "command",
17509
- value: trimmed,
17510
- context: `plugin_file:${fileName}`
17511
- });
17512
- }
17513
- }
17514
- }
17515
- if (JS_TS_EXTENSIONS.has(ext)) {
17516
- artifacts.push(...extractCommandsFromJsTs(content, fileName));
17517
- }
17518
- return artifacts;
17519
- }
17520
- async function scanPlugin(plugin, threats, options = {}) {
17521
- const { checkUrls = true, checkFileHashes = true, trustedDomains, logger = nullLogger } = options;
19029
+ async function scanPlugin(plugin, options = {}) {
19030
+ const { checkUrls = true, checkFileHashes = true, checkSkills = true, amsiClient = null, logger = nullLogger } = options;
17522
19031
  const result = { plugin, findings: [] };
19032
+ const skillCheckPromise = checkSkills ? runSkillCheck(plugin, result.findings, logger) : Promise.resolve();
17523
19033
  const files = await walkPluginFiles(plugin.installPath, logger);
17524
- if (files.length === 0)
19034
+ if (files.length === 0) {
19035
+ await skillCheckPromise;
17525
19036
  return result;
17526
- const commandThreats = threats.filter((t) => t.matchOn.has("command"));
17527
- const heuristics = new HeuristicsEngine(commandThreats, trustedDomains);
19037
+ }
17528
19038
  const allUrls = [];
17529
19039
  const hashToFiles = /* @__PURE__ */ new Map();
17530
19040
  for (const filePath of files) {
@@ -17536,26 +19046,29 @@ async function scanPlugin(plugin, threats, options = {}) {
17536
19046
  } catch {
17537
19047
  continue;
17538
19048
  }
17539
- const artifacts = extractArtifactsFromFile(filePath, content);
17540
- if (artifacts.length > 0) {
17541
- const matches = heuristics.match(artifacts);
17542
- for (const match of matches) {
17543
- result.findings.push({
17544
- threatId: match.threat.id,
17545
- title: match.threat.title,
17546
- severity: match.threat.severity,
17547
- confidence: match.threat.confidence,
17548
- action: match.threat.action,
17549
- artifact: match.artifact.slice(0, 200),
17550
- sourceFile: (0, import_node_path13.relative)(plugin.installPath, filePath)
17551
- });
19049
+ if (amsiClient) {
19050
+ try {
19051
+ const scanName = `${plugin.key}/${(0, import_node_path18.relative)(plugin.installPath, filePath)}`;
19052
+ const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
19053
+ if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
19054
+ result.findings.push({
19055
+ threatId: "AMSI_SCAN",
19056
+ title: `AMSI detection (result=${amsiResult.amsiResult})`,
19057
+ severity: "critical",
19058
+ confidence: 1,
19059
+ action: "block",
19060
+ artifact: content.slice(0, 200),
19061
+ sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
19062
+ });
19063
+ }
19064
+ } catch {
17552
19065
  }
17553
19066
  }
17554
19067
  if (checkUrls) {
17555
19068
  allUrls.push(...extractUrls(content));
17556
19069
  }
17557
19070
  if (checkFileHashes) {
17558
- const sha256 = (0, import_node_crypto9.createHash)("sha256").update(rawBytes).digest("hex");
19071
+ const sha256 = (0, import_node_crypto10.createHash)("sha256").update(rawBytes).digest("hex");
17559
19072
  const existing = hashToFiles.get(sha256);
17560
19073
  if (existing) {
17561
19074
  existing.push(filePath);
@@ -17602,7 +19115,7 @@ async function scanPlugin(plugin, threats, options = {}) {
17602
19115
  confidence: 1,
17603
19116
  action: "block",
17604
19117
  artifact: fr.sha256,
17605
- sourceFile: (0, import_node_path13.relative)(plugin.installPath, filePath)
19118
+ sourceFile: (0, import_node_path18.relative)(plugin.installPath, filePath)
17606
19119
  });
17607
19120
  }
17608
19121
  }
@@ -17610,11 +19123,44 @@ async function scanPlugin(plugin, threats, options = {}) {
17610
19123
  } catch {
17611
19124
  }
17612
19125
  })() : Promise.resolve();
17613
- await Promise.all([urlCheckPromise, fileCheckPromise]);
19126
+ await Promise.all([urlCheckPromise, fileCheckPromise, skillCheckPromise]);
17614
19127
  return result;
17615
19128
  }
19129
+ async function runSkillCheck(plugin, findings, logger) {
19130
+ try {
19131
+ const skills = await computeSkillIdsForRoot(plugin.installPath);
19132
+ if (skills.length === 0)
19133
+ return;
19134
+ const ids = skills.map((s) => s.skillId);
19135
+ const client = new SkillCheckClient(void 0, logger);
19136
+ const verdicts = await client.checkSkills(ids);
19137
+ for (const { folder, skillId } of skills) {
19138
+ const verdict = verdicts.get(skillId);
19139
+ if (!verdict)
19140
+ continue;
19141
+ const risk = (verdict.overallRiskLevel ?? "").toUpperCase();
19142
+ if (risk !== "HIGH" && risk !== "CRITICAL")
19143
+ continue;
19144
+ const severity = risk === "CRITICAL" ? "critical" : "high";
19145
+ findings.push({
19146
+ threatId: "SKILL_CHECK",
19147
+ title: verdict.summary?.trim() || `Risky skill detected (${risk})`,
19148
+ severity,
19149
+ confidence: 1,
19150
+ action: "log",
19151
+ artifact: skillId.slice(0, 16),
19152
+ sourceFile: (0, import_node_path18.relative)(plugin.installPath, folder) || ".",
19153
+ recommendations: verdict.recommendations.length > 0 ? verdict.recommendations : void 0
19154
+ });
19155
+ }
19156
+ } catch (e) {
19157
+ logger.debug("Skill check failed", { error: String(e) });
19158
+ }
19159
+ }
17616
19160
 
17617
19161
  // ../core/dist/session-start.js
19162
+ var import_node_child_process2 = require("node:child_process");
19163
+ var import_node_fs4 = require("node:fs");
17618
19164
  init_config();
17619
19165
  init_file_utils();
17620
19166
 
@@ -17629,7 +19175,8 @@ function fromCachedFinding(finding) {
17629
19175
  confidence: finding.confidence,
17630
19176
  action: finding.action,
17631
19177
  artifact: finding.artifact,
17632
- sourceFile: finding.source_file
19178
+ sourceFile: finding.source_file,
19179
+ recommendations: finding.recommendations
17633
19180
  };
17634
19181
  }
17635
19182
  function toFindingData(finding) {
@@ -17640,7 +19187,8 @@ function toFindingData(finding) {
17640
19187
  confidence: finding.confidence,
17641
19188
  action: finding.action,
17642
19189
  artifact: finding.artifact,
17643
- source_file: finding.sourceFile
19190
+ source_file: finding.sourceFile,
19191
+ recommendations: finding.recommendations
17644
19192
  };
17645
19193
  }
17646
19194
  function toAuditFindingData(finding) {
@@ -17649,23 +19197,39 @@ function toAuditFindingData(finding) {
17649
19197
  }
17650
19198
  async function runSessionStartScan(context) {
17651
19199
  const logger = context.logger ?? nullLogger;
17652
- const threats = await loadThreats(context.threatsDir, logger);
17653
- const trustedDomains = await loadTrustedDomains(context.allowlistsDir, logger);
17654
- if (threats.length === 0) {
17655
- return [];
17656
- }
19200
+ const sageConfig = await loadConfig(context.configPath, logger);
17657
19201
  const plugins = context.plugins;
17658
19202
  if (plugins.length === 0) {
17659
19203
  return [];
17660
19204
  }
17661
19205
  let exceptions = [];
17662
19206
  try {
17663
- const sageConfig = await loadConfig(context.configPath, logger);
17664
19207
  exceptions = await loadExceptions(sageConfig.exceptions, logger);
17665
19208
  } catch {
17666
19209
  }
19210
+ let amsiClient = null;
19211
+ if (sageConfig.amsi_check.enabled && isAmsiSupported()) {
19212
+ try {
19213
+ amsiClient = new AmsiClient(logger);
19214
+ await amsiClient.init();
19215
+ if (!amsiClient.isAvailable) {
19216
+ amsiClient.close();
19217
+ amsiClient = null;
19218
+ }
19219
+ } catch {
19220
+ amsiClient?.close();
19221
+ amsiClient = null;
19222
+ }
19223
+ }
19224
+ try {
19225
+ return await scanAllPlugins(context, sageConfig, plugins, exceptions, amsiClient, logger);
19226
+ } finally {
19227
+ amsiClient?.close();
19228
+ }
19229
+ }
19230
+ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient, logger) {
17667
19231
  const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.allowlistsDir);
17668
- const cache = await loadScanCache(configHash, context.scanCachePath, logger);
19232
+ const cache2 = await loadScanCache(configHash, context.scanCachePath, logger);
17669
19233
  const resultsWithFindings = [];
17670
19234
  let cacheModified = false;
17671
19235
  for (const plugin of plugins) {
@@ -17691,7 +19255,7 @@ async function runSessionStartScan(context) {
17691
19255
  if (allowMatch) {
17692
19256
  continue;
17693
19257
  }
17694
- const cached = getCached(cache, plugin.key, plugin.version, plugin.lastUpdated);
19258
+ const cached = getCached(cache2, plugin.key, plugin.version, plugin.lastUpdated);
17695
19259
  if (cached && cached.findings.length === 0) {
17696
19260
  continue;
17697
19261
  }
@@ -17702,25 +19266,24 @@ async function runSessionStartScan(context) {
17702
19266
  });
17703
19267
  continue;
17704
19268
  }
17705
- const result = await scanPlugin(plugin, threats, {
19269
+ const result = await scanPlugin(plugin, {
17706
19270
  checkUrls: context.checkUrls ?? true,
17707
19271
  checkFileHashes: context.checkFileHashes ?? true,
17708
- trustedDomains,
19272
+ amsiClient,
17709
19273
  logger
17710
19274
  });
17711
- storeResult(cache, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
19275
+ storeResult(cache2, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
17712
19276
  cacheModified = true;
17713
19277
  if (result.findings.length > 0) {
17714
19278
  resultsWithFindings.push(result);
17715
19279
  }
17716
19280
  }
17717
19281
  if (cacheModified) {
17718
- await saveScanCache(cache, context.scanCachePath, logger);
19282
+ await saveScanCache(cache2, context.scanCachePath, logger);
17719
19283
  }
17720
19284
  try {
17721
- const sageConfig = await loadConfig(context.configPath, logger);
17722
19285
  for (const result of resultsWithFindings) {
17723
- await logPluginScan(sageConfig.logging, result.plugin.key, result.plugin.version, result.findings.map(toAuditFindingData));
19286
+ await logPluginScan(config2.logging, result.plugin.key, result.plugin.version, result.findings.map(toAuditFindingData));
17724
19287
  }
17725
19288
  } catch {
17726
19289
  }
@@ -17732,7 +19295,7 @@ init_types2();
17732
19295
 
17733
19296
  // ../core/dist/version-check.js
17734
19297
  init_types2();
17735
- var DEFAULT_TIMEOUT_MS2 = 5e3;
19298
+ var DEFAULT_TIMEOUT_MS3 = 5e3;
17736
19299
  function isNewerVersion(current, latest) {
17737
19300
  const parse = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
17738
19301
  const cur = parse(current);
@@ -17747,7 +19310,7 @@ function isNewerVersion(current, latest) {
17747
19310
  }
17748
19311
  return false;
17749
19312
  }
17750
- async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = DEFAULT_TIMEOUT_MS2, context) {
19313
+ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = DEFAULT_TIMEOUT_MS3, context) {
17751
19314
  if (currentVersion === "dev") {
17752
19315
  logger.debug("Skipping version check for dev build");
17753
19316
  return null;
@@ -17757,6 +19320,14 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
17757
19320
  logger.debug("Skipping version check: missing installation id");
17758
19321
  return null;
17759
19322
  }
19323
+ const envelope = buildSageProxyEnvelope({
19324
+ iid: context.iid,
19325
+ versionApp: currentVersion,
19326
+ agentRuntime: context.agentRuntime,
19327
+ agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown"
19328
+ });
19329
+ const extendedInfo = await loadExtendedInfo(void 0, logger).catch(() => null);
19330
+ const enriched = mergeExtendedInfo(envelope, extendedInfo);
17760
19331
  const response = await fetch(resolveEndpoint("/v2/version-check"), {
17761
19332
  method: "POST",
17762
19333
  signal: AbortSignal.timeout(timeoutMs),
@@ -17764,12 +19335,7 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
17764
19335
  Accept: "application/json",
17765
19336
  "Content-Type": "application/json"
17766
19337
  },
17767
- body: JSON.stringify(buildSageProxyEnvelope({
17768
- iid: context.iid,
17769
- versionApp: currentVersion,
17770
- agentRuntime: context.agentRuntime,
17771
- agentRuntimeVersion: context.agentRuntimeVersion ?? "unknown"
17772
- }))
19338
+ body: JSON.stringify(enriched)
17773
19339
  });
17774
19340
  if (!response.ok) {
17775
19341
  logger.debug(`Version check HTTP ${response.status}`);
@@ -17798,6 +19364,16 @@ async function runSessionStart(ctx) {
17798
19364
  const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
17799
19365
  pruneOrphanedTmpFiles(sageDirPath).catch(() => {
17800
19366
  });
19367
+ maybeSpawnModelDownloadWorker({
19368
+ sageDirPath,
19369
+ workerPath: ctx.modelDownloadWorkerPath,
19370
+ configPath: ctx.configPath,
19371
+ agentRuntime: ctx.agentRuntime,
19372
+ agentRuntimeVersion: ctx.agentRuntimeVersion,
19373
+ versionApp: ctx.version,
19374
+ logger
19375
+ }).catch(() => {
19376
+ });
17801
19377
  const iidPromise = getInstallationId(sageDirPath).catch(() => void 0);
17802
19378
  const [scanResults, versionCheck] = await Promise.all([
17803
19379
  runSessionStartScan({
@@ -17824,10 +19400,57 @@ async function runSessionStart(ctx) {
17824
19400
  ]);
17825
19401
  return { scanResults, versionCheck };
17826
19402
  }
19403
+ async function maybeSpawnModelDownloadWorker(args) {
19404
+ if (!args.workerPath)
19405
+ return;
19406
+ if (!(0, import_node_fs4.existsSync)(args.workerPath)) {
19407
+ args.logger.debug(`model download worker script not found at ${args.workerPath}`);
19408
+ return;
19409
+ }
19410
+ if (!anyRequiredModelMissing(MODEL_SCHEMA_VERSION, args.sageDirPath))
19411
+ return;
19412
+ let piEnabled = false;
19413
+ try {
19414
+ const config2 = await loadConfig(args.configPath, args.logger);
19415
+ piEnabled = config2.pi_check.enabled && !config2.pi_check.model_path;
19416
+ } catch {
19417
+ piEnabled = false;
19418
+ }
19419
+ if (!piEnabled)
19420
+ return;
19421
+ spawnModelDownloadWorker({
19422
+ sageDirPath: args.sageDirPath,
19423
+ workerPath: args.workerPath,
19424
+ agentRuntime: args.agentRuntime,
19425
+ agentRuntimeVersion: args.agentRuntimeVersion,
19426
+ versionApp: args.versionApp,
19427
+ logger: args.logger
19428
+ });
19429
+ }
19430
+ function spawnModelDownloadWorker(args) {
19431
+ const logger = args.logger ?? nullLogger;
19432
+ try {
19433
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [args.workerPath], {
19434
+ detached: true,
19435
+ stdio: "ignore",
19436
+ env: {
19437
+ ...process.env,
19438
+ SAGE_DIR: args.sageDirPath,
19439
+ SAGE_AGENT_RUNTIME: String(args.agentRuntime ?? "unknown"),
19440
+ SAGE_AGENT_RUNTIME_VERSION: args.agentRuntimeVersion ?? "",
19441
+ SAGE_VERSION_APP: args.versionApp ?? "",
19442
+ SAGE_MODEL_SCHEMA: MODEL_SCHEMA_VERSION
19443
+ }
19444
+ });
19445
+ child.unref();
19446
+ } catch (err) {
19447
+ logger.debug(`failed to spawn model download worker: ${err}`);
19448
+ }
19449
+ }
17827
19450
 
17828
19451
  // ../core/dist/scan-handler.js
17829
- async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2 = defaultBranding) {
17830
- logger.info(`${branding2.product_name} plugin scan started (${context})`, {
19452
+ async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2 = defaultBranding, modelDownloadWorkerPath, style = "verbose") {
19453
+ logger.info(`${branding2.name} plugin scan started (${context})`, {
17831
19454
  threatsDir,
17832
19455
  allowlistsDir
17833
19456
  });
@@ -17837,27 +19460,28 @@ async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir
17837
19460
  allowlistsDir,
17838
19461
  version,
17839
19462
  logger,
17840
- agentRuntime
19463
+ agentRuntime,
19464
+ modelDownloadWorkerPath
17841
19465
  });
17842
- logger.info(`${branding2.product_name} plugin scan (${context}) complete`, {
19466
+ logger.info(`${branding2.name} plugin scan (${context}) complete`, {
17843
19467
  findings: result.scanResults.length,
17844
19468
  updateAvailable: result.versionCheck?.updateAvailable ?? false
17845
19469
  });
17846
- return formatSessionStartMessage(version, result, branding2);
19470
+ return formatSessionStartMessage(version, result, branding2, style);
17847
19471
  }
17848
19472
  function createScanHandler(options) {
17849
- const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, allowlistsDir, version, agentRuntime, branding: branding2 = defaultBranding, onResult } = options;
19473
+ const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, allowlistsDir, version, agentRuntime, branding: branding2 = defaultBranding, onResult, modelDownloadWorkerPath, style = "verbose" } = options;
17850
19474
  return async () => {
17851
19475
  try {
17852
19476
  let plugins = await discoverPlugins2();
17853
19477
  plugins = plugins.filter((p) => !p.key.startsWith(selfPrefix));
17854
19478
  if (plugins.length === 0) {
17855
- logger.warn(`${branding2.product_name} plugin scan (${context}): no plugins to scan after filtering`);
19479
+ logger.warn(`${branding2.name} plugin scan (${context}): no plugins to scan after filtering`);
17856
19480
  }
17857
- const msg = await runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2);
19481
+ const msg = await runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime, branding2, modelDownloadWorkerPath, style);
17858
19482
  onResult?.(msg);
17859
19483
  } catch (e) {
17860
- logger.error(`${branding2.product_name} ${context} scan failed`, { error: String(e) });
19484
+ logger.error(`${branding2.name} ${context} scan failed`, { error: String(e) });
17861
19485
  }
17862
19486
  };
17863
19487
  }
@@ -17866,19 +19490,19 @@ function createScanHandler(options) {
17866
19490
  init_types2();
17867
19491
 
17868
19492
  // src/bundled-dirs.ts
17869
- var import_node_path14 = require("node:path");
17870
- var packageRoot = (0, import_node_path14.resolve)(__dirname, "..");
19493
+ var import_node_path19 = require("node:path");
19494
+ var packageRoot = (0, import_node_path19.resolve)(__dirname, "..");
17871
19495
  function getBundledDataDirs() {
17872
19496
  return {
17873
- threatsDir: (0, import_node_path14.join)(packageRoot, "resources", "threats"),
17874
- allowlistsDir: (0, import_node_path14.join)(packageRoot, "resources", "allowlists")
19497
+ threatsDir: (0, import_node_path19.join)(packageRoot, "resources", "threats"),
19498
+ allowlistsDir: (0, import_node_path19.join)(packageRoot, "resources", "allowlists")
17875
19499
  };
17876
19500
  }
17877
19501
  var cachedVersion;
17878
19502
  function getSageVersion() {
17879
19503
  if (cachedVersion) return cachedVersion;
17880
19504
  try {
17881
- const pkg = JSON.parse(getFileContentSync((0, import_node_path14.join)(packageRoot, "package.json"), "utf-8"));
19505
+ const pkg = JSON.parse(getFileContentSync((0, import_node_path19.join)(packageRoot, "package.json"), "utf-8"));
17882
19506
  cachedVersion = pkg.version ?? "0.0.0";
17883
19507
  } catch {
17884
19508
  cachedVersion = "0.0.0";
@@ -17907,35 +19531,38 @@ function createLogger(pluginLogger) {
17907
19531
  };
17908
19532
  }
17909
19533
 
19534
+ // src/startup-scan.ts
19535
+ var import_node_path21 = require("node:path");
19536
+
17910
19537
  // src/plugin-discovery.ts
17911
- var import_promises7 = require("node:fs/promises");
17912
- var import_node_os2 = require("node:os");
17913
- var import_node_path15 = require("node:path");
17914
- var DEFAULT_EXTENSIONS_DIR = (0, import_node_path15.join)((0, import_node_os2.homedir)(), ".openclaw", "extensions");
19538
+ var import_promises9 = require("node:fs/promises");
19539
+ var import_node_os4 = require("node:os");
19540
+ var import_node_path20 = require("node:path");
19541
+ var DEFAULT_EXTENSIONS_DIR = (0, import_node_path20.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
17915
19542
  async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding) {
17916
- logger.info(`${branding2.product_name} plugin discovery: scanning extensions directory`, {
19543
+ logger.info(`${branding2.name} plugin discovery: scanning extensions directory`, {
17917
19544
  path: extensionsDir
17918
19545
  });
17919
19546
  let entries;
17920
19547
  try {
17921
- entries = await (0, import_promises7.readdir)(extensionsDir);
19548
+ entries = await (0, import_promises9.readdir)(extensionsDir);
17922
19549
  } catch {
17923
19550
  logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
17924
19551
  return [];
17925
19552
  }
17926
19553
  const plugins = [];
17927
19554
  for (const entry of entries) {
17928
- const extDir = (0, import_node_path15.join)(extensionsDir, entry);
19555
+ const extDir = (0, import_node_path20.join)(extensionsDir, entry);
17929
19556
  let stats;
17930
19557
  try {
17931
- stats = await (0, import_promises7.stat)(extDir);
19558
+ stats = await (0, import_promises9.stat)(extDir);
17932
19559
  if (!stats.isDirectory()) continue;
17933
19560
  } catch {
17934
19561
  continue;
17935
19562
  }
17936
19563
  let pkg;
17937
19564
  try {
17938
- const pkgPath = (0, import_node_path15.join)(extDir, "package.json");
19565
+ const pkgPath = (0, import_node_path20.join)(extDir, "package.json");
17939
19566
  const raw = await getFileContent(pkgPath);
17940
19567
  pkg = JSON.parse(raw);
17941
19568
  } catch {
@@ -17945,7 +19572,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
17945
19572
  const name = pkg.name ?? entry;
17946
19573
  const version = pkg.version ?? "unknown";
17947
19574
  const key = `${name}@${version}`;
17948
- logger.debug(`${branding2.product_name} plugin discovery: found extension`, {
19575
+ logger.debug(`${branding2.name} plugin discovery: found extension`, {
17949
19576
  key,
17950
19577
  path: extDir
17951
19578
  });
@@ -17956,7 +19583,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
17956
19583
  lastUpdated: stats.mtime.toISOString()
17957
19584
  });
17958
19585
  }
17959
- logger.info(`${branding2.product_name} plugin discovery: found ${plugins.length} extension(s)`);
19586
+ logger.info(`${branding2.name} plugin discovery: found ${plugins.length} extension(s)`);
17960
19587
  return plugins;
17961
19588
  }
17962
19589
 
@@ -17974,7 +19601,8 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
17974
19601
  version,
17975
19602
  agentRuntime: "openclaw",
17976
19603
  branding: branding2,
17977
- onResult
19604
+ onResult,
19605
+ modelDownloadWorkerPath: (0, import_node_path21.resolve)(__dirname, "model-download-worker.cjs")
17978
19606
  });
17979
19607
  }
17980
19608
  function createStartupScanHandler(logger, branding2 = defaultBranding, onResult) {
@@ -17988,7 +19616,7 @@ function createBeforeAgentStartHandler(getFindings, clearFindings, logger, brand
17988
19616
  const findings = getFindings();
17989
19617
  if (!findings) return void 0;
17990
19618
  clearFindings();
17991
- logger.info(`${branding2.product_name}: surfacing plugin scan findings via before_agent_start`);
19619
+ logger.info(`${branding2.name}: surfacing plugin scan findings via before_agent_start`);
17992
19620
  const prependContext = [
17993
19621
  '<security-alert source="sage-plugin-scan">',
17994
19622
  findings,
@@ -18001,6 +19629,15 @@ function createBeforeAgentStartHandler(getFindings, clearFindings, logger, brand
18001
19629
  }
18002
19630
 
18003
19631
  // src/tool-handler.ts
19632
+ var OPENCLAW_TOOL_MAP = {
19633
+ bash: "Bash",
19634
+ exec: "Bash",
19635
+ web_fetch: "WebFetch",
19636
+ write: "Write",
19637
+ edit: "Edit",
19638
+ read: "Read",
19639
+ apply_patch: "ApplyPatch"
19640
+ };
18004
19641
  function extractFilePaths(patch) {
18005
19642
  const artifacts = [];
18006
19643
  for (const line of patch.split("\n")) {
@@ -18011,6 +19648,23 @@ function extractFilePaths(patch) {
18011
19648
  }
18012
19649
  return artifacts;
18013
19650
  }
19651
+ function normalizeToolInput(toolName, params) {
19652
+ switch (toolName) {
19653
+ case "write":
19654
+ return {
19655
+ ...params,
19656
+ file_path: params.path ?? "",
19657
+ content: params.content ?? ""
19658
+ };
19659
+ case "edit":
19660
+ return {
19661
+ ...params,
19662
+ file_path: params.path ?? ""
19663
+ };
19664
+ default:
19665
+ return params;
19666
+ }
19667
+ }
18014
19668
  function mapToolToArtifacts(toolName, params) {
18015
19669
  switch (toolName) {
18016
19670
  case "bash":
@@ -18048,8 +19702,8 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
18048
19702
  sessionId,
18049
19703
  conversationId: sessionId,
18050
19704
  agentRuntime: "openclaw",
18051
- toolName,
18052
- toolInput: params,
19705
+ toolName: canonicalizeToolName(OPENCLAW_TOOL_MAP, toolName),
19706
+ toolInput: normalizeToolInput(toolName, params),
18053
19707
  artifacts
18054
19708
  },
18055
19709
  { threatsDir, allowlistsDir, logger },
@@ -18065,7 +19719,7 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
18065
19719
  return {
18066
19720
  requireApproval: {
18067
19721
  id: actionId,
18068
- title: `${branding2.product_name}: ${verdict.category}`,
19722
+ title: `${branding2.name}: ${verdict.category}`,
18069
19723
  description: [
18070
19724
  `Severity: ${verdict.severity}`,
18071
19725
  `Reason: ${reasons}`,
@@ -18096,10 +19750,11 @@ function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir,
18096
19750
  }
18097
19751
 
18098
19752
  // src/index.ts
18099
- var branding = loadBrandingSync();
19753
+ var config = loadConfigSync();
19754
+ var branding = resolveBranding(config.brand_key);
18100
19755
  var index_default = {
18101
19756
  id: "sage-openclaw",
18102
- name: branding.product_name,
19757
+ name: branding.name,
18103
19758
  description: "Safety for Agents \u2014 ADR layer that guards commands, files, and web requests",
18104
19759
  configSchema: {
18105
19760
  jsonSchema: { type: "object", additionalProperties: false, properties: {} }