@devilsdev/rag-guardrails 3.8.0 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +255 -26
  2. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -184,7 +184,7 @@ var require_injection_detector = __commonJS({
184
184
  // Chat-template role markers: "system:" as a line prefix, [system],
185
185
  // <|im_start|>system, [INST], <<SYS>>, "### system".
186
186
  id: "role-marker",
187
- pattern: /(?:^|\n)\s*(?:system|assistant|developer)\s*:|\[\s*(?:system|inst)\s*\]|<\|?\s*(?:im_start|system)\s*\|?>|<<\s*sys\s*>>|#{2,}\s*(?:system|instruction)s?\b/
187
+ pattern: /(?:^|\n)\s*(?:system|assistant|developer)\s*:|\[\s*(?:system|inst)\s*\]|<\|?\s*(?:im_start|system)\s*\|?>|<<\s*sys\s*>>|(?<!#)#{2,}\s*(?:system|instruction)s?\b/
188
188
  },
189
189
  {
190
190
  id: "dan",
@@ -256,7 +256,7 @@ var require_detectors = __commonJS({
256
256
  var { detectInjection } = require_injection_detector();
257
257
  var KINDS = ["injection", "pii"];
258
258
  var DEFAULT_PII_PATTERNS = {
259
- email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
259
+ email: /(?<![a-zA-Z0-9._%+-])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
260
260
  phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
261
261
  ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
262
262
  creditCard: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g
@@ -1040,12 +1040,20 @@ code{background:var(--chip);padding:1px 5px;border-radius:4px;font-size:13px}`;
1040
1040
  }
1041
1041
  });
1042
1042
 
1043
+ // scoped:@devilsdev/rag-core|retrieval/access-fields.js
1044
+ var require_access_fields = __commonJS({
1045
+ "scoped:@devilsdev/rag-core|retrieval/access-fields.js"(exports2, module2) {
1046
+ module2.exports = require("@devilsdev/rag-core")[Symbol.for("@devilsdev/rag-core/internal")]("retrieval/access-fields.js");
1047
+ }
1048
+ });
1049
+
1043
1050
  // src/guardrails/retrieval-guard.js
1044
1051
  var require_retrieval_guard = __commonJS({
1045
1052
  "src/guardrails/retrieval-guard.js"(exports2, module2) {
1046
1053
  "use strict";
1047
1054
  var { EventEmitter } = require("events");
1048
1055
  var { detectInjection } = require_injection_detector();
1056
+ var { NO_ACCESS } = require_access_fields();
1049
1057
  var {
1050
1058
  resolveDetectors,
1051
1059
  runDetectorsSync,
@@ -1173,17 +1181,15 @@ var require_retrieval_guard = __commonJS({
1173
1181
  }
1174
1182
  }
1175
1183
  if (!dominated && this.config.enforceACL) {
1176
- const userPermissions = context.userPermissions || [];
1177
- const restriction = result.requiredRole ?? result.acl ?? doc.requiredRole ?? doc.acl;
1178
- const requiredRoles = restriction ? [].concat(restriction) : null;
1179
- if (requiredRoles && requiredRoles.length > 0) {
1180
- const hasAccess = requiredRoles.some(
1181
- (role) => userPermissions.includes(role)
1182
- );
1183
- if (!hasAccess) {
1184
- dominated = true;
1185
- reasons.acl = (reasons.acl || 0) + 1;
1186
- }
1184
+ const userPermissions = permissionsOf(context.userPermissions).filter(
1185
+ (role) => role !== NO_ACCESS
1186
+ );
1187
+ const hasAccess = restrictionsOf(result).every(
1188
+ (roles) => roles.some((role) => userPermissions.includes(role))
1189
+ );
1190
+ if (!hasAccess) {
1191
+ dominated = true;
1192
+ reasons.acl = (reasons.acl || 0) + 1;
1187
1193
  }
1188
1194
  }
1189
1195
  dropped[i] = dominated;
@@ -1247,6 +1253,28 @@ var require_retrieval_guard = __commonJS({
1247
1253
  result.content || result.text || result.pageContent || doc.content || doc.text || ""
1248
1254
  );
1249
1255
  }
1256
+ function restrictionsOf(result) {
1257
+ if (!result || typeof result !== "object") return [];
1258
+ const lists = [];
1259
+ for (const source of [result, result.metadata, result.doc]) {
1260
+ if (!source || typeof source !== "object") continue;
1261
+ for (const key of ["requiredRole", "acl"]) {
1262
+ const value = source[key];
1263
+ if (value === void 0 || value === null || value === "") continue;
1264
+ const roles = [].concat(value);
1265
+ if (roles.length > 0) lists.push(roles);
1266
+ }
1267
+ }
1268
+ return lists;
1269
+ }
1270
+ function permissionsOf(value) {
1271
+ if (typeof value === "string") return [value];
1272
+ if (Array.isArray(value)) return value;
1273
+ if (value && typeof value[Symbol.iterator] === "function") {
1274
+ return Array.from(value);
1275
+ }
1276
+ return [];
1277
+ }
1250
1278
  module2.exports = { RetrievalGuard, DEFAULT_CONFIG };
1251
1279
  }
1252
1280
  });
@@ -1489,6 +1517,13 @@ var require_post_generation_guard = __commonJS({
1489
1517
  }
1490
1518
  });
1491
1519
 
1520
+ // scoped:@devilsdev/rag-core|utils/run-abort.js
1521
+ var require_run_abort = __commonJS({
1522
+ "scoped:@devilsdev/rag-core|utils/run-abort.js"(exports2, module2) {
1523
+ module2.exports = require("@devilsdev/rag-core")[Symbol.for("@devilsdev/rag-core/internal")]("utils/run-abort.js");
1524
+ }
1525
+ });
1526
+
1492
1527
  // src/guardrails/guardrails-pipeline.js
1493
1528
  var require_guardrails_pipeline = __commonJS({
1494
1529
  "src/guardrails/guardrails-pipeline.js"(exports2, module2) {
@@ -1498,6 +1533,7 @@ var require_guardrails_pipeline = __commonJS({
1498
1533
  var { RetrievalGuard } = require_retrieval_guard();
1499
1534
  var { PostGenerationGuard } = require_post_generation_guard();
1500
1535
  var { detectedText } = require_detectors();
1536
+ var { createRunAbort } = require_run_abort();
1501
1537
  var DEFAULT_CONFIG = {
1502
1538
  preRetrieval: {},
1503
1539
  // PreRetrievalGuard options, or false to disable
@@ -1508,12 +1544,79 @@ var require_guardrails_pipeline = __commonJS({
1508
1544
  strict: false
1509
1545
  // If true, block on any guard failure; if false, warn and continue
1510
1546
  };
1547
+ var DEFAULT_MAX_STREAM_CHARS = 1e6;
1548
+ var RETRIEVAL_SKIPPED_REASON = "retrieval guard could not run: the wrapped pipeline neither called filterResults nor returned results; answer withheld";
1511
1549
  function summarizeFilter(filterOutput) {
1512
1550
  return {
1513
1551
  ...filterOutput,
1514
1552
  removed: filterOutput.removed.map((doc) => ({ id: doc.id }))
1515
1553
  };
1516
1554
  }
1555
+ function isAsyncIterable(value) {
1556
+ return value != null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";
1557
+ }
1558
+ function withheldSummary(guardrails) {
1559
+ const out = { ...guardrails };
1560
+ if (out.preRetrieval) {
1561
+ const { sanitizedQuery, injection, ...rest } = out.preRetrieval;
1562
+ out.preRetrieval = {
1563
+ ...rest,
1564
+ ...injection && {
1565
+ injection: {
1566
+ detected: injection.detected,
1567
+ matches: (injection.matches || []).map((m) => ({ id: m && m.id }))
1568
+ }
1569
+ }
1570
+ };
1571
+ }
1572
+ if (out.retrieval && Array.isArray(out.retrieval.results)) {
1573
+ const { results, ...rest } = out.retrieval;
1574
+ out.retrieval = {
1575
+ ...rest,
1576
+ removed: (rest.removed || []).map((doc) => ({ id: doc && doc.id }))
1577
+ };
1578
+ }
1579
+ if (out.postGeneration) {
1580
+ out.postGeneration = { ...out.postGeneration, sanitizedOutput: "" };
1581
+ }
1582
+ return out;
1583
+ }
1584
+ function withheld(error, guardrails) {
1585
+ return { success: false, error, guardrails: withheldSummary(guardrails) };
1586
+ }
1587
+ async function collectStream(source, { signal, timeout, maxChars }) {
1588
+ const iterator = source[Symbol.asyncIterator]();
1589
+ const run = createRunAbort({ timeout, signal });
1590
+ let text = "";
1591
+ let done = null;
1592
+ let overflow = false;
1593
+ try {
1594
+ for (; ; ) {
1595
+ const next = await run.race(iterator.next());
1596
+ if (next.done) break;
1597
+ const chunk = next.value;
1598
+ text += typeof chunk === "string" ? chunk : String((chunk && chunk.token) ?? "");
1599
+ if (chunk && typeof chunk === "object" && chunk.done) {
1600
+ const { token, ...rest } = chunk;
1601
+ done = rest;
1602
+ break;
1603
+ }
1604
+ if (text.length > maxChars) {
1605
+ overflow = true;
1606
+ break;
1607
+ }
1608
+ }
1609
+ } catch (error) {
1610
+ if (iterator.return) Promise.resolve(iterator.return()).catch(() => {
1611
+ });
1612
+ throw error;
1613
+ } finally {
1614
+ run.cleanup();
1615
+ }
1616
+ if (iterator.return) await Promise.resolve(iterator.return()).catch(() => {
1617
+ });
1618
+ return { text, done, overflow };
1619
+ }
1517
1620
  function withReasons(message, check) {
1518
1621
  const issues = check && check.issues || [];
1519
1622
  if (issues.length === 0) return message;
@@ -1545,6 +1648,12 @@ var require_guardrails_pipeline = __commonJS({
1545
1648
  /**
1546
1649
  * Run the full guarded pipeline.
1547
1650
  *
1651
+ * With `options.stream` and a post-generation guard, the token stream is
1652
+ * buffered, checked and redacted as a whole, then released as an async
1653
+ * iterable: one token chunk with the sanitized answer, then a done chunk
1654
+ * carrying the redacted citations/evaluation and the `guardrails` summary.
1655
+ * A strict-mode block yields only a done chunk with `success: false`.
1656
+ *
1548
1657
  * @param {object} input - Pipeline input
1549
1658
  * @param {string} input.query - The user query
1550
1659
  * @param {Float32Array} [input.queryVector] - Optional precomputed query vector
@@ -1553,6 +1662,7 @@ var require_guardrails_pipeline = __commonJS({
1553
1662
  * @returns {Promise<object>} Pipeline result with guardrails metadata attached
1554
1663
  */
1555
1664
  async run(input) {
1665
+ const startedAt = Date.now();
1556
1666
  const guardrails = {
1557
1667
  preRetrieval: null,
1558
1668
  retrieval: null,
@@ -1568,14 +1678,13 @@ var require_guardrails_pipeline = __commonJS({
1568
1678
  );
1569
1679
  this.emit("preRetrieval", guardrails.preRetrieval);
1570
1680
  if (!guardrails.preRetrieval.safe && this.config.strict) {
1571
- return {
1572
- success: false,
1573
- error: withReasons(
1681
+ return withheld(
1682
+ withReasons(
1574
1683
  "Pre-retrieval guard blocked query",
1575
1684
  guardrails.preRetrieval
1576
1685
  ),
1577
1686
  guardrails
1578
- };
1687
+ );
1579
1688
  }
1580
1689
  query = guardrails.preRetrieval.sanitizedQuery;
1581
1690
  }
@@ -1595,10 +1704,11 @@ var require_guardrails_pipeline = __commonJS({
1595
1704
  query,
1596
1705
  options: { ...input.options, citations: true, ...filterOptions }
1597
1706
  });
1707
+ const streaming = isAsyncIterable(result);
1598
1708
  if (this.retrievalGuard && preGenerationFilter) {
1599
1709
  guardrails.retrieval = summarizeFilter(preGenerationFilter);
1600
1710
  this.emit("retrieval", guardrails.retrieval);
1601
- } else if (this.retrievalGuard && result.results) {
1711
+ } else if (this.retrievalGuard && !streaming && result && Array.isArray(result.results)) {
1602
1712
  guardrails.retrieval = summarizeFilter(
1603
1713
  await this.retrievalGuard.filterAsync(
1604
1714
  result.results,
@@ -1609,12 +1719,29 @@ var require_guardrails_pipeline = __commonJS({
1609
1719
  this.emit("retrieval", guardrails.retrieval);
1610
1720
  result.results = guardrails.retrieval.results;
1611
1721
  if (guardrails.retrieval.metadata.reasons.acl) {
1612
- return {
1613
- success: false,
1614
- error: "Retrieval guard removed access-controlled documents after generation; answer withheld",
1722
+ return withheld(
1723
+ "Retrieval guard removed access-controlled documents after generation; answer withheld",
1615
1724
  guardrails
1616
- };
1725
+ );
1726
+ }
1727
+ } else if (this.retrievalGuard && this._retrievalChecksActive() && result && (streaming || result.answer || this.retrievalGuard.config.enforceACL && result.success !== false)) {
1728
+ guardrails.retrieval = {
1729
+ skipped: true,
1730
+ reason: RETRIEVAL_SKIPPED_REASON
1731
+ };
1732
+ if (this.retrievalGuard.config.enforceACL || this.config.strict) {
1733
+ return withheld(RETRIEVAL_SKIPPED_REASON, guardrails);
1617
1734
  }
1735
+ this._warnRetrievalSkipped();
1736
+ }
1737
+ if (streaming && this.postGuard) {
1738
+ return this._releaseStream(result, {
1739
+ guardrails,
1740
+ callOptions,
1741
+ signal,
1742
+ timeout: this._remainingTimeout(input, startedAt),
1743
+ docs: preGenerationFilter ? preGenerationFilter.results : void 0
1744
+ });
1618
1745
  }
1619
1746
  if (this.postGuard && result.answer) {
1620
1747
  guardrails.postGeneration = await this.postGuard.checkAsync(
@@ -1624,14 +1751,13 @@ var require_guardrails_pipeline = __commonJS({
1624
1751
  );
1625
1752
  this.emit("postGeneration", guardrails.postGeneration);
1626
1753
  if (!guardrails.postGeneration.safe && this.config.strict) {
1627
- return {
1628
- success: false,
1629
- error: withReasons(
1754
+ return withheld(
1755
+ withReasons(
1630
1756
  "Post-generation guard flagged output",
1631
1757
  guardrails.postGeneration
1632
1758
  ),
1633
1759
  guardrails
1634
- };
1760
+ );
1635
1761
  }
1636
1762
  result.answer = guardrails.postGeneration.sanitizedOutput;
1637
1763
  const found = detectedText(guardrails.postGeneration);
@@ -1641,6 +1767,109 @@ var require_guardrails_pipeline = __commonJS({
1641
1767
  result.guardrails = guardrails;
1642
1768
  return result;
1643
1769
  }
1770
+ /**
1771
+ * Whether the retrieval guard has a check that needs documents.
1772
+ * @private
1773
+ */
1774
+ _retrievalChecksActive() {
1775
+ const c = this.retrievalGuard.config;
1776
+ return Boolean(
1777
+ c.enforceACL || c.detectInjection || c.minRelevanceScore > 0 || c.maxAgeDays > 0
1778
+ );
1779
+ }
1780
+ /**
1781
+ * Milliseconds left of the caller's options.timeout, or undefined.
1782
+ * @private
1783
+ */
1784
+ _remainingTimeout(input, startedAt) {
1785
+ const t = input.options && input.options.timeout;
1786
+ if (typeof t !== "number" || !(t > 0)) return void 0;
1787
+ return Math.max(1, t - (Date.now() - startedAt));
1788
+ }
1789
+ /** @private One warning per pipeline instance. */
1790
+ _warnRetrievalSkipped() {
1791
+ if (this._warnedRetrievalSkipped) return;
1792
+ this._warnedRetrievalSkipped = true;
1793
+ process.emitWarning(
1794
+ "GuardrailsPipeline: the retrieval guard was skipped because the wrapped pipeline neither called options.filterResults nor returned results. Call options.filterResults before generating, or return results.",
1795
+ { code: "RAG_GUARDRAILS_RETRIEVAL_SKIPPED" }
1796
+ );
1797
+ }
1798
+ /**
1799
+ * Collect a token stream, run the post-generation guard on the whole
1800
+ * answer and return an async iterable that yields the sanitized answer as
1801
+ * one token chunk and then the done chunk. A blocked answer yields no
1802
+ * tokens, only a done chunk with the error.
1803
+ *
1804
+ * @private
1805
+ */
1806
+ async _releaseStream(source, { guardrails, callOptions, signal, timeout, docs }) {
1807
+ const limit = this.postGuard.config.maxResponseLength;
1808
+ const maxChars = limit > 0 ? limit : DEFAULT_MAX_STREAM_CHARS;
1809
+ const { text, done, overflow } = await collectStream(source, {
1810
+ signal,
1811
+ timeout,
1812
+ maxChars
1813
+ });
1814
+ let tokenChunk = null;
1815
+ let doneChunk;
1816
+ if (!text) {
1817
+ doneChunk = { ...done, token: "", done: true, guardrails };
1818
+ } else {
1819
+ const check = await this.postGuard.checkAsync(
1820
+ text,
1821
+ done && done.results || docs,
1822
+ callOptions
1823
+ );
1824
+ if (overflow && check.safe) {
1825
+ check.safe = false;
1826
+ check.sanitizedOutput = check.sanitizedOutput.slice(0, maxChars);
1827
+ check.issues.push(
1828
+ `Output exceeds maximum length of ${maxChars} characters`
1829
+ );
1830
+ }
1831
+ guardrails.postGeneration = check;
1832
+ this.emit("postGeneration", check);
1833
+ if (!check.safe && this.config.strict) {
1834
+ doneChunk = {
1835
+ token: "",
1836
+ done: true,
1837
+ ...withheld(
1838
+ withReasons("Post-generation guard flagged output", check),
1839
+ guardrails
1840
+ ),
1841
+ ...done && done.timing && { timing: done.timing }
1842
+ };
1843
+ } else {
1844
+ const found = detectedText(check);
1845
+ const { citations, evaluation, ...rest } = done || {};
1846
+ doneChunk = {
1847
+ ...rest,
1848
+ token: "",
1849
+ done: true,
1850
+ ...citations !== void 0 && {
1851
+ citations: this._redactDeep(citations, found)
1852
+ },
1853
+ ...evaluation !== void 0 && {
1854
+ evaluation: this._redactDeep(evaluation, found)
1855
+ },
1856
+ guardrails
1857
+ };
1858
+ if (check.sanitizedOutput) {
1859
+ tokenChunk = { token: check.sanitizedOutput, done: false };
1860
+ }
1861
+ }
1862
+ }
1863
+ const stream = (async function* released() {
1864
+ if (tokenChunk) yield tokenChunk;
1865
+ yield doneChunk;
1866
+ })();
1867
+ Object.defineProperty(stream, "guardrails", {
1868
+ value: doneChunk.guardrails,
1869
+ enumerable: false
1870
+ });
1871
+ return stream;
1872
+ }
1644
1873
  /**
1645
1874
  * Citations and evaluation details copy sentences from the raw answer, so
1646
1875
  * scrub every string in them with the same PII patterns as the answer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devilsdev/rag-guardrails",
3
- "version": "3.8.0",
3
+ "version": "3.8.2",
4
4
  "description": "Guardrails for RAG: prompt-injection detection and pre-retrieval, retrieval and post-generation guards.",
5
5
  "keywords": [
6
6
  "rag",
@@ -42,6 +42,6 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@devilsdev/rag-core": "3.8.0"
45
+ "@devilsdev/rag-core": "3.8.2"
46
46
  }
47
47
  }