مفتوح المصدر · ROSH™ Company Labs

llm-stream-guardrails حجب البيانات الشخصية والأسرار أثناء البثّ

مكتبة واحدة حتى الآن: llm-stream-guardrails تزيل البيانات الشخصية والمفاتيح السرّية من ردّ النموذج اللغوي أثناء بثّه — دون تخزينه مؤقّتًا أولًا، وبنتيجة مطابقة بايت ببايت لتصفية النص بعد اكتماله.

مجانية ومرخّصة برخصة MIT. إن وفّرت عليك وقتًا:
رخصة MIT صفر اعتماديات TypeScript، ESM، Node 18+ منشورة مع إثبات مصدر موثّق

لماذا يتجاوز البثّ معظم أدوات الحماية

يرسل النموذج إجابته على أجزاء صغيرة. إذا فحصت كل جزء بمفرده، فإن ما يمتدّ بين جزأين يمرّ دون أن يُرى:

chunk 1:  "...your card is 4111 11"
chunk 2:  "11 1111 1111, charge it."

لا يحتوي أيّ من الجزأين على رقم بطاقة، لكنهما معًا يحتويانه — وعندما يكتمل الردّ يكون القارئ قد رآه أصلًا. الحلّ الشائع هو التوقّف عن البثّ: تخزين الإجابة كاملة، ثم ترشيحها، ثم إرسالها. وهذا تنازل عن السبب الوحيد للبثّ.

بيانات مقسومة بين جزأي SSE: ثغرة معروفة

يتكرّر الإبلاغ عن العطل نفسه في أدوات كثيرة:

  • LiteLLM #41611، 17 سبتمبر 2026 — «Streaming guardrails: value split across two SSE chunks can pass per-chunk checks» — أي أن قيمة مقسومة بين جزأي SSE تجتاز الفحص الجاري على كل جزء.
  • Mastra #23783، 13 سبتمبر 2026 — «PIIDetector emits PII in the clear when a match is split across stream chunks» — أي أن البيانات الشخصية تخرج مكشوفة حين تنقسم المطابقة بين جزأين.
  • LangChain #35011، 4 فبراير 2026 — «Streaming bypasses guardrails/middleware» — أي أن البثّ يتجاوز الوسيط فلا يُنفَّذ على الرموز.

وحتى سبتمبر 2026 كان Vercel AI SDK يقول ذلك داخل مثال الحماية نفسه: «streaming guardrails are difficult to implement, because you do not know the full content of the stream until it's finished» — أي أن الحماية أثناء البثّ صعبة التنفيذ لأن المحتوى الكامل غير معروف قبل انتهاء البثّ. وقد اختفى هذا التعليق لأن المثال نُفِّذ: بتخزين كل كتلة نصّية مؤقّتًا حتى نهايتها، وهو ما تذكر الوثائق أنه يؤخّر الإخراج.

الضمان: ناتج مطابق بايت ببايت مهما كان حجم الجزء

لأي مُدخل، وأي سياسة، وأي حجم جزء، يكون الناتج المبثوث مطابقًا بايت ببايت لتصفية النص كاملًا دفعة واحدة — وكل جزء يخرج من المكتبة سليم البنية بذاته.

هذا الضمان مُختبَر في التكامل المستمر باختبار خصائص يجمع بين المُدخلات والسياسات وأحجام الأجزاء، لا مجرّد وعد في ملف README. والعرض المباشر هو النسخة المختصرة من البرهان: اخفض حجم الجزء إلى حرف واحد، ولن يتغيّر الناتج.

افتح العرض المباشر — يشغّل النسخة المنشورة داخل متصفّحك، ولا يغادر ما تكتبه جهازك.

التثبيت — TypeScript وESM وNode 18+

npm install llm-stream-guardrails
import { sieve } from 'llm-stream-guardrails';

for await (const token of sieve(result.textStream)) {
  process.stdout.write(token);   // secrets already gone
}

ESM فقط، وNode 18 أو أحدث. يقبل أي مُكرِّر غير متزامن من النصوص: textStream الخاص بـVercel AI SDK مباشرة، وحزمتا OpenAI وAnthropic بسطر واحد لاستخراج النص من كائنات الأحداث. ولـWeb Streams وبيئات edge هناك createSieveTransform() وهي TransformStream.

ما الذي تكتشفه: بيانات شخصية ومفاتيح API وأسرار

  • عناوين البريد الإلكتروني وأرقام الهاتف وأرقام الضمان الاجتماعي الأمريكية.
  • بطاقات الدفع بالتحقّق من خوارزمية Luhn، وحسابات IBAN بالتحقّق من mod-97 — فرقم طلب من 16 خانة يبقى دون مساس.
  • 35 شكلًا من بيانات الاعتماد، منها مفاتيح OpenAI وAnthropic وStripe وGitHub وGitLab وAWS وGoogle وSlack وSendGrid وnpm، ورموز JWT، وكتل المفاتيح الخاصة PEM كاملة.
  • أسماء الحقول كما القيم، لأن النموذج يكتب السجلّات في شكل أسطر معنونة.
  • عناوين IP، اختياريًا، لكثرة ورودها في المخرجات التقنية.

يجري الاكتشاف على صورة معيارية للنص، فلا يستطيع رقم بعرض كامل، ولا مسافة غير فاصلة، ولا محرف بعرض صفري، ولا حرف مشابه الشكل، أن يمرّر قيمة أمام نمط مكتوب بحروف ASCII.

ما ليست هي

هذه طبقة دفاع إضافية، وليست أداة امتثال قانوني.

إنها تطابق الأشكال لا المعاني: سرّ مكتوب بالكلمات، أو شكل اعتماد ابتُكر الشهر الماضي، يمرّ. نسبة الاستدعاء المقاسة على مجموعات بيانات خارجية لم تُكتب هنا تتراوح بين 47% و81%، لا 100%. وهي غير معتمدة وفق GDPR أو HIPAA أو PCI-DSS أو أي إطار آخر، ولا يُدّعى ذلك هنا: فتلك خصائص نظام ومشغّله، لا خصائص اعتمادية برمجية. ضعها أمام بثّ لا ينبغي أن يحمل أسرارًا، وواصل كل ما تفعله أصلًا.

أرقام

  • 79 اختبارًا، منها مجموعة اختراق هجومية واختبار خصائص يقارن الناتج المبثوث بتصفية النص كاملًا عند كل حدّ بين الأجزاء.
  • صفر نتائج إيجابية خاطئة على مجموعة نظيفة من 39 عيّنة — معرّفات برمجية وأسعار وأرقام طلبات وحالات شبيهة متعمّدة يجب أن تمرّ سليمة.
  • الإصدار 0.7.6، منشور مع إثبات مصدر موثّق، بحيث يمكن ربط الحزمة بمسار العمل الذي بناها.
  • الأسبوع الأول على npm (21–22 سبتمبر 2026): 353 تنزيلًا، و268 استنساخًا للمستودع من 95 مستخدمًا مختلفًا.

مساهمة في مشروع خارجي

أثناء كتابة المكتبة ظهرت مشكلتان في توثيق Language Model Middleware الخاص بـVercel AI SDK: مثال الحماية أثناء البثّ ترك wrapStream دون تنفيذ، وأمثلة الوسيط الخمسة جميعها أغفلت حقلًا مطلوبًا. المسألة #21209 أبلغت عنهما.

ثلاثة طلبات دمج تشير إليها دُمجت في 22 سبتمبر 2026 — رقم 21210 على main، ورقم 21211 على release-v6.0، ورقم 21212 على release-v5.0. فتحتها أتمتة المشروع نفسه، والـcommit المدموج على الفرع الأساسي يحمل Co-authored-by: roshcompanylabs.

صار لهذا الصنف من العيوب اسم

في 22 سبتمبر 2026 نشر Ninad Phalak تقرير Split-Boundary Leaks in Streaming Guardrails (Zenodo، رخصة CC BY 4.0، doi.org/10.5281/zenodo.22909585): وهو تقرير يجمع أربع تطبيقات مستقلة للعطل نفسه — LiteLLM وMastra وLangChain وNVIDIA NeMo Guardrails — إلى جانب المثال الذي تُرك دون تنفيذ في توثيق Vercel AI SDK. ويسمّي الشرط الثابت الذي تفتقر إليه جميعها: «لأي طريقة لتقسيم المُدخل نفسه، يجب أن يكون الناتج المبثوث مجمّعًا مطابقًا بايت ببايت لتصفية النص كاملًا دفعة واحدة».

هذا هو الضمان المذكور في صدر هذه الصفحة، والتصميم الذي يصفه التقرير بأنه الوحيد الذي يحافظ على البثّ مع تحقيق ذلك الشرط — حساب نقطة حسم عند كل جزء — هو التصميم الذي تنفّذه هذه المكتبة. ويوثّق التقرير أيضًا مصدر تصحيح التوثيق لدى Vercel، فينسبه صراحةً إلى هذا الحساب: مشرف خارجي لمكتبة حجب أثناء البثّ أبلغ في 20 سبتمبر 2026 أن أمثلة الوسيط لا تُترجم برمجيًا وأن مثال الحماية أثناء البثّ تُرك دون تنفيذ.

ويستحق سؤالان مفتوحان فيه الذكر، لأن هذا المشروع يجيب عن أحدهما. يشير التقرير إلى أن لا مشروع ينشر «ثبات النتيجة مهما كان التقسيم» كعقد معلن، وأن كلفة هذا التصميم في زمن الاستجابة غير مقيسة في أي تطبيق. وهذا الشرط هو العقد المعلن لهذه المكتبة، وأرقام الاحتجاز في الأسئلة الشائعة مأخوذة من مقياس الأداء المشحون مع الحزمة. والتقرير لا يفحص هذه المكتبة ولا يقول عنها شيئًا؛ وإنما يُستشهد به هنا لأنه سمّى الصنف. وقد تناولنا الصنف كاملًا — المشاريع الأربعة والشرط الثابت والاختبار الذي يكشفه — في بيانات مقسومة بين جزأين.

المشرف

يشرف عليها Redouane — ROSH™ Company Labs. تُرسل الأخطاء وطلبات الميزات إلى مسائل GitHub. وأنفع بلاغ يمكن أن يصل هذا المشروع هو طريقة لتجاوز أحد الكواشف؛ ويشرح SECURITY.md كيف تُرسل على نحو خاص.

FAQ

أسئلة يطرحها المطوّرون

أداة الحماية عندي تفحص كل جزء، ومع ذلك مرّ بريد إلكتروني كامل. كيف؟

كل جزء يُفحص بمفرده، فقيمة مقسومة إلى «user@exa» و«mple.com» لا تطابق شيئًا في أيٍّ من النصفين — ثم يعيد المتصفّح تجميعها على الشاشة. لا بدّ أن يجري الفحص على النص المتراكم لا على كل دفعة على حدة.

الوسيط عندي يكتشف القيمة فعلًا، لكن المستخدم رآها. لماذا لا ينفع الحجب؟

البثّ من وجهة نظر القارئ إضافةٌ لا رجعة فيها. ما خرج من رموز لا يمكن سحبه، فالحكم الذي يصل بعد الرموز يغيّر سجلّاتك لا الشاشة. القرار يجب أن يسبق خروج البايتات.

هل يمكن فحص المحتوى أولًا ثم بثّه؟

نعم، إن حجزت فقط ما قد يتحوّل إلى مطابقة. تُخرج المكتبة كل بايت انتهى فحصه وتحتفظ بالذيل غير المحسوم، فيبدأ الناتج فورًا بدل انتظار اكتمال التوليد.

هل تخزّن الردّ كاملًا مؤقّتًا؟ وكم تكلّف من زمن؟

لا. في النص المكتوب بمسافات تحتفظ بنحو عشرين محرفًا: الوسيط 18 والمتوسّط 18 عبر 1792 استدعاءً لـpush() على أربعة نصوص مكتوبة بمسافات وثمانية أحجام أجزاء، أُعيد قياسها على 0.7.6 — ومقياس الأداء يُشحن مع الحزمة عبر npm run latency. أما اليابانية والصينية والتايلندية فلا تحتفظ فيها بشيء تقريبًا: ردّ ياباني من 330 محرفًا لم يحتفظ بشيء وبدأ الإخراج من المحرف الأول، لأن الحرف الصيني/الياباني لا يمكن أن يكون جزءًا من أي بيانات اعتماد تكشفها المكتبة. وقرب قيمة قد تطول تحتفظ بأكثر عمدًا؛ ولا يوجد أي إعداد لحجم التخزين المؤقّت، فلا تستطيع أي تهيئة أن تُحدث تسريبًا.

هل تبثّ النص الياباني أو الصيني أو الكوري أو التايلندي؟

نعم، منذ 0.7.3 — وليس قبلها. في 0.7.2 كان المحرّك يعامل الفقرة الخالية من المسافات كوحدة واحدة غير قابلة للقطع فلا يُطلقها إلا في النهاية: ردّ ياباني من 2640 محرفًا لم يُخرج شيئًا قبل أن ينتهي. وبالقياس هنا على 0.7.3، لا يحتفظ الردّ نفسه بأي محرف ويبدأ الإخراج من المحرف الأول؛ كما أن بريدًا إلكترونيًا ورقم بطاقة ومفتاح API داخل نص ياباني تُحجب تمامًا كما لو رُشّح النص كاملًا، عند كل أحجام الأجزاء المُختبرة. وإن كنت مثبّتًا على 0.7.2 فحدّث.

ما الضمان بالضبط؟

لأي مُدخل وأي سياسة وأي تقسيم، يكون الناتج المبثوث مجمّعًا مطابقًا بايت ببايت لتصفية النص كاملًا، وكل جزء يخرج سليم البنية بذاته. هذه خاصية في المحرّك، لا وعد ببذل الجهد.

كيف جرى اختبار ذلك؟

اختبار خصائص يعيد تشغيل المُدخلات عبر السياسات وأحجام الأجزاء — بما فيها أجزاء بمحرف واحد وقطوع داخل المطابقة نفسها — ويتحقّق أن الناتج المجمّع يساوي نتيجة النص الكامل. وفي المشروع 79 اختبارًا، منها مجموعة هجومية.

أليست مجرّد حجز N بايت من الذيل؟

لا. الحجز الثابت يفشل كلما تجاوزت المطابقة ذلك الحد، ويفشل صامتًا: يبدو الناتج محجوبًا. هنا تُشتقّ نقطة الحسم من الأنماط نفسها: أبعد موضع لا يمكن لأي مطابقة أن تمتدّ خلفه.

هل تعمل مع Vercel AI SDK أو LangChain أو خلف وسيط؟

هي تحويل للتدفّق لا تكامل مع إطار بعينه، فتوضع حيث تمسك الأجزاء أصلًا: داخل wrapStream، أو في معالج وسيط، أو حول أي مُكرِّر غير متزامن من النصوص. وcreateSieveTransform() يمنحك TransformStream لبيئات Web Streams وedge.

هل أستطيع تغيير ما تكتشفه وما يحدث عند المطابقة؟

نعم. الإجراء إمّا mask أو block أو report؛ ويمكنك استبدال مجموعة الكواشف، وإضافة كلمات ممنوعة، وتمرير دالة حجب خاصة بك، وتلقّي نداء onDetect يقع مرّة واحدة بالضبط لكل اكتشاف ومعه القيمة كاملة.

هل تستطيع حيل Unicode تمرير قيمة؟

يجري الاكتشاف على صورة معيارية للنص، فتُطوى الأرقام كاملة العرض والمسافات غير الفاصلة والمحارف عديمة العرض والحروف المشابهة قبل المطابقة. ويمكن إيقاف التوحيد، وإيقافه يجعل تجاوز الكواشف سهلًا.

ما الذي لا تكتشفه؟

تطابق الأشكال لا المعاني: سرّ مكتوب بالكلمات، أو شكل اعتماد ابتُكر الشهر الماضي، يمرّ؛ والنمط الملتصق بمحارف كلمة يُترك عمدًا. ونسبة الاستدعاء المقاسة على مجموعات خارجية بين 47% و81%.

هل هي أداة امتثال لـGDPR أو HIPAA أو PCI-DSS؟

لا، ولا يُدّعى ذلك. هي طبقة واحدة على مسار الإخراج — دفاع في العمق. والامتثال خاصية نظام ومشغّله، لا خاصية اعتمادية برمجية.

هل تُفسد استدعاءات الأدوات إذا انقسمت الوسائط بين جزأين؟

هي ترشّح التدفّق الذي تعطيه إيّاها، فأعطها قناة النص. JSON استدعاء الأداة ينقسم بين الأجزاء تمامًا كالنثر، وحجب شيء داخل الوسائط يُفسد الاستدعاء — رشّح تلك القناة على حدة، أو اتركها.

هل تحتفظ بنافذة منزلقة بين الأجزاء أم تفحص كل جزء وحده؟

تحتفظ بالنص المتراكم وتعيد حساب نقطة الحسم مع كل جزء: أبعد موضع لا تستطيع أي مطابقة أن تمتدّ خلفه. لا توجد نافذة ثابتة، والمطابقة التي تلامس نهاية المخزن لا تُعدّ نهائية أبدًا.

مثال wrapStream في AI SDK كان فارغًا. فماذا يوضع فيه؟

منذ تصحيح التوثيق في سبتمبر 2026 صار فيها مثال عامل: يخزّن كل كتلة نصّية مؤقّتًا حتى text-end ثم يحجب النص كاملًا، فيسدّ ثغرة المطابقة المقسومة — لكنه، كما تذكر الوثائق، يؤخّر الإخراج ويستهلك ذاكرة تتناسب مع حجم الكتلة. وإن أردت إبقاء التدفّق جاريًا، مرّر دفعات النص عبر sieve() داخل wrapStream ثم أعد إخراجها.

لماذا لا نستدعي الواجهة غير المتدفّقة ثم نعيد الردّ كرموز مُصطنعة؟

هذا ينجح، وينصح به بعض المشرفين، لكنه يجعل زمن أول رمز مساويًا لزمن التوليد الكامل — وهو ما وُجد البثّ لتفاديه. أما حجز الذيل غير المحسوم وحده فيُبقي التدفّق تدفّقًا.

ما الذي أُصلح منذ النشر؟

أربعة أمور، كلّها مذكورة في سجلّ التغييرات. 0.7.3: النص المكتوب بلا مسافات كان يُحتجز حتى النهاية بدل أن يُبثّ. 0.7.4: الحارس الذي يمنع القطع بين الحرف وعلامته كان يعرف العلامات اللاتينية وحدها، فلم تُعامَل حركات العربية ولا نقاط العبرية ولا حركات الديفاناغاري والتايلندية معاملة العلامات. 0.7.5: أحد الأنماط كان بلا حدّ، فاستغرق فحص 8000 محرف متّصل 846 مللي ثانية مقابل 3 للنثر العادي — وقِيس هنا بـ18 مللي ثانية بعد الإصلاح — كما أن رمز JWT بحمولة تتجاوز 1024 محرفًا لم يكن يُكتشف إطلاقًا. 0.7.6: من يخفض maxRetention كان بوسعه طلب سقف أقصر من أطول مطابقة ممكنة، وهذه التهيئة صارت غير قابلة للطلب. ولم تلتقط مجموعة الاختبارات أيًّا من الأربعة قبل النشر؛ جاء كل واحد منها من سؤال أو قياس لاحق، وصار لكلٍّ منها اختبار الآن.

ما مدى نضجها؟

نُشرت أول مرة في 21 سبتمبر 2026، ثم صدرت 0.7.3 في اليوم التالي لإصلاح البثّ في اللغات التي تُكتب بلا مسافات. الضمان مُصاغ بدقّة وتغطّيه 79 اختبارًا يمكنك قراءتها، لكنها لم تُشغَّل على نطاق واسع خارج جهاز مؤلّفها. اقرأ مجموعة الاختبارات قبل الاعتماد عليها، وافتح مسألة إن تجاوزت أحد الكواشف.