@devilsdev/rag-guardrails 3.8.1 → 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 +227 -16
  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,7 +1181,9 @@ var require_retrieval_guard = __commonJS({
1173
1181
  }
1174
1182
  }
1175
1183
  if (!dominated && this.config.enforceACL) {
1176
- const userPermissions = permissionsOf(context.userPermissions);
1184
+ const userPermissions = permissionsOf(context.userPermissions).filter(
1185
+ (role) => role !== NO_ACCESS
1186
+ );
1177
1187
  const hasAccess = restrictionsOf(result).every(
1178
1188
  (roles) => roles.some((role) => userPermissions.includes(role))
1179
1189
  );
@@ -1507,6 +1517,13 @@ var require_post_generation_guard = __commonJS({
1507
1517
  }
1508
1518
  });
1509
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
+
1510
1527
  // src/guardrails/guardrails-pipeline.js
1511
1528
  var require_guardrails_pipeline = __commonJS({
1512
1529
  "src/guardrails/guardrails-pipeline.js"(exports2, module2) {
@@ -1516,6 +1533,7 @@ var require_guardrails_pipeline = __commonJS({
1516
1533
  var { RetrievalGuard } = require_retrieval_guard();
1517
1534
  var { PostGenerationGuard } = require_post_generation_guard();
1518
1535
  var { detectedText } = require_detectors();
1536
+ var { createRunAbort } = require_run_abort();
1519
1537
  var DEFAULT_CONFIG = {
1520
1538
  preRetrieval: {},
1521
1539
  // PreRetrievalGuard options, or false to disable
@@ -1526,12 +1544,79 @@ var require_guardrails_pipeline = __commonJS({
1526
1544
  strict: false
1527
1545
  // If true, block on any guard failure; if false, warn and continue
1528
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";
1529
1549
  function summarizeFilter(filterOutput) {
1530
1550
  return {
1531
1551
  ...filterOutput,
1532
1552
  removed: filterOutput.removed.map((doc) => ({ id: doc.id }))
1533
1553
  };
1534
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
+ }
1535
1620
  function withReasons(message, check) {
1536
1621
  const issues = check && check.issues || [];
1537
1622
  if (issues.length === 0) return message;
@@ -1563,6 +1648,12 @@ var require_guardrails_pipeline = __commonJS({
1563
1648
  /**
1564
1649
  * Run the full guarded pipeline.
1565
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
+ *
1566
1657
  * @param {object} input - Pipeline input
1567
1658
  * @param {string} input.query - The user query
1568
1659
  * @param {Float32Array} [input.queryVector] - Optional precomputed query vector
@@ -1571,6 +1662,7 @@ var require_guardrails_pipeline = __commonJS({
1571
1662
  * @returns {Promise<object>} Pipeline result with guardrails metadata attached
1572
1663
  */
1573
1664
  async run(input) {
1665
+ const startedAt = Date.now();
1574
1666
  const guardrails = {
1575
1667
  preRetrieval: null,
1576
1668
  retrieval: null,
@@ -1586,14 +1678,13 @@ var require_guardrails_pipeline = __commonJS({
1586
1678
  );
1587
1679
  this.emit("preRetrieval", guardrails.preRetrieval);
1588
1680
  if (!guardrails.preRetrieval.safe && this.config.strict) {
1589
- return {
1590
- success: false,
1591
- error: withReasons(
1681
+ return withheld(
1682
+ withReasons(
1592
1683
  "Pre-retrieval guard blocked query",
1593
1684
  guardrails.preRetrieval
1594
1685
  ),
1595
1686
  guardrails
1596
- };
1687
+ );
1597
1688
  }
1598
1689
  query = guardrails.preRetrieval.sanitizedQuery;
1599
1690
  }
@@ -1613,10 +1704,11 @@ var require_guardrails_pipeline = __commonJS({
1613
1704
  query,
1614
1705
  options: { ...input.options, citations: true, ...filterOptions }
1615
1706
  });
1707
+ const streaming = isAsyncIterable(result);
1616
1708
  if (this.retrievalGuard && preGenerationFilter) {
1617
1709
  guardrails.retrieval = summarizeFilter(preGenerationFilter);
1618
1710
  this.emit("retrieval", guardrails.retrieval);
1619
- } else if (this.retrievalGuard && result.results) {
1711
+ } else if (this.retrievalGuard && !streaming && result && Array.isArray(result.results)) {
1620
1712
  guardrails.retrieval = summarizeFilter(
1621
1713
  await this.retrievalGuard.filterAsync(
1622
1714
  result.results,
@@ -1627,12 +1719,29 @@ var require_guardrails_pipeline = __commonJS({
1627
1719
  this.emit("retrieval", guardrails.retrieval);
1628
1720
  result.results = guardrails.retrieval.results;
1629
1721
  if (guardrails.retrieval.metadata.reasons.acl) {
1630
- return {
1631
- success: false,
1632
- 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",
1633
1724
  guardrails
1634
- };
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);
1635
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
+ });
1636
1745
  }
1637
1746
  if (this.postGuard && result.answer) {
1638
1747
  guardrails.postGeneration = await this.postGuard.checkAsync(
@@ -1642,14 +1751,13 @@ var require_guardrails_pipeline = __commonJS({
1642
1751
  );
1643
1752
  this.emit("postGeneration", guardrails.postGeneration);
1644
1753
  if (!guardrails.postGeneration.safe && this.config.strict) {
1645
- return {
1646
- success: false,
1647
- error: withReasons(
1754
+ return withheld(
1755
+ withReasons(
1648
1756
  "Post-generation guard flagged output",
1649
1757
  guardrails.postGeneration
1650
1758
  ),
1651
1759
  guardrails
1652
- };
1760
+ );
1653
1761
  }
1654
1762
  result.answer = guardrails.postGeneration.sanitizedOutput;
1655
1763
  const found = detectedText(guardrails.postGeneration);
@@ -1659,6 +1767,109 @@ var require_guardrails_pipeline = __commonJS({
1659
1767
  result.guardrails = guardrails;
1660
1768
  return result;
1661
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
+ }
1662
1873
  /**
1663
1874
  * Citations and evaluation details copy sentences from the raw answer, so
1664
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.1",
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.1"
45
+ "@devilsdev/rag-core": "3.8.2"
46
46
  }
47
47
  }