Day 13 我用 Promptfoo 反覆測惡意 SOP,但還有一種文件根本不需要藏指令:它只要屬於別的使用者、部門或 tenant,被當前 Agent 搜出來就已經出問題。
這種失敗不一定會出現在最後回答。未授權文件只要進入 prompt,內容就可能出現在 trace、cache、日誌或後續工具參數。因此我不能等模型回答後才刪掉敏感字句。在 retrieval 開始以前,候選文件就必須受權限限制。
Live 模式的原則是:模型永遠排在文件授權之後。ChatOpenAI 不負責判斷使用者能否讀文件,也不應看到被拒絕的標題、摘要或 chunk。
server-side principal
→ tenant filter
→ document ACL
→ retrieval
→ post-retrieval authorization
→ content rail
→ Live LLM
Day 14 的 Live 實驗現在使用 server-side principal campus-a / student。程式先從三份文件中擋下其他 tenant 與 admin-only 文件,再把唯一獲准的 VPN SOP 組進模型訊息。只要前面任一步拒絕,文件內容就不會出現在真實模型的 context。
後端驗證的 principal
→ tenant 與文件 ACL 前置過濾
→ 只對已授權候選集合做 retrieval
→ 取回後再次授權
→ NeMo Retrieval Rail 檢查內容
→ model-visible context
這裡有兩種很容易混在一起的檢查:
| 邊界 | 回答的問題 |
|---|---|
| Tenant/ACL 授權 | 這個身分有沒有權限讀這份文件? |
| Retrieval Rail | 這份已授權文件的內容,適不適合放進模型 context? |
NeMo Guardrails 官方將 Retrieval Rails 放在 RAG retrieval 完成之後,用來處理 retrieved chunks。這個位置很適合做指令型內容、敏感資料或其他 content checks,但它不會自動知道我應用程式的 tenant 或文件 ACL。NVIDIA NeMo Guardrails:Guardrails Configuration
Day 14 的 mock principal 由後端建立:
principal = Principal(
user_id="student.demo",
tenant_id="tenant-a",
roles=frozenset({"helpdesk"}),
)
tenant_id 和 roles 不會從使用者 prompt 或 SOP 內容推測。如果真實系統有登入機制,這些資料應該來自經驗證的 session、token claims 或後端 identity service。
這次候選資料有四份:
| 文件 | Tenant | ACL | 前置決定 |
|---|---|---|---|
SOP-A-VPN-001 |
A | helpdesk/admin | 允許。 |
SOP-A-VPN-INJECT-001 |
A | helpdesk | 授權通過,留給後面 content rail 檢查。 |
SOP-A-ADMIN-001 |
A | admin | 當前角色不符,不進入 retrieval。 |
SOP-B-VPN-001 |
B | helpdesk | 不同 tenant,不進入 retrieval。 |
因此 vector search 或其他相似度搜尋只會在前兩份文件中進行。這不只是把最後結果遮住,而是讓未授權文件一開始就不參與排名與 context 組裝。
Azure AI Search 的 document-level access control 也是在 indexing 保留權限 metadata,並在 query time 依使用者或群組身分排除未授權文件。我的 mock 不是 Azure 實作,但它要表達的邊界相同:權限是 query 的條件,不是 prompt 裡的建議。Azure AI Search:Document-Level Access Control
只做前置過濾,意味著我完全相信 retriever 的實作不會改壞、cache 不會混到別的 tenant,而且權限在查詢期間不會變動。這些條件都可能失效。
所以我在 demo 裡故意模擬 retriever 錯誤塞回 SOP-B-VPN-001。取回後的授權複檢不看結果排名,只重新核對 server-controlled principal 和文件 metadata;這份跨 tenant 文件會留下 post_retrieval_authorization blocked,然後被移除。
前置過濾是主防線,檢索後複檢是 defense in depth。如果實際搜尋服務支援原生 ACL 或 security filter,我會優先讓它在 query time 執行,而不是先取回全部內容再靠 Python 刪除。
經過 tenant 與 ACL 後,SOP-A-VPN-INJECT-001 仍然是當前身分有權限讀取的文件。但它的內容含有假造 SYSTEM MESSAGE,所以還不應進入模型 context。
我在 guardrails/day14/config.yml 放了 NeMo Guardrails 0.23.0 的 regex Retrieval Rail 設定:
rails:
config:
regex_detection:
retrieval:
patterns:
- '(system|developer)\s*(prompt|message)'
- 'ignore.{0,32}(previous|prior|all).{0,20}(instructions?|rules?)'
- '忽略.{0,16}(指令|規則)'
case_insensitive: true
retrieval:
flows:
- regex check retrieval
NVIDIA 官方文件對 regex check retrieval 的行為定義很具體:命中 retrieval patterns 的 chunks 會被移除,只剩下未命中的 chunks 作為 context。NVIDIA NeMo Guardrails:Regex Detection Integration
本機頁面為了不需要 API key,會讀取同一份 YAML 並做 deterministic preview。它不會假裝 NeMo runtime 已經執行。要實際用 NeMo 0.23.0 載入設定,可以安裝 optional dependency 後執行:
uv pip install -e '.[guardrails]'
python -m app.validate_nemo_config
預期會列出:
NeMo config loaded: regex check retrieval
這些 regex 只是可解釋的入門案例,不是完整的 injection detector。我會繼續用 Day 13 的 corpus 抓已知繞法,也會在後面比較其他 retrieval rails 與紅隊測試。

固定情境從四份 mock 文件開始。前置授權剩下兩份,檢索後故意混入的跨 tenant 結果被複檢擋下,指令型內容又被 Retrieval Rail 預覽隔離,最後只剩一份文件可進入 context。
我新增的單元測試會分開檢查 tenant、ACL、檢索後複檢和 NeMo YAML patterns。完整 suite 是 92 個 unit tests 通過,Promptfoo 是 15 passed、0 failed、0 errors。
這些結果證明固定 mock 流程保留了授權順序,不代表生產環境的 identity provider、向量資料庫、cache 與 ACL 同步都已經安全。真實系統還要處理權限變更的延遲、default deny、審計日誌,以及後端服務帳號是否擁有過大權限。
Day 15 會從文件邊界轉到工具邊界:當 Agent 同時看到太多功能重疊的工具,為什麼不是能力變強,而是選錯工具的機會變多。
day-14-live-document-authorization