安全出站管制、知识库与新闻订阅
30 秒导读: 这一章讲的是围绕研究核心的三件外围事。第一件是安全边界——LDR 允许你把研究"关"在一个出站范围里(只走公网 / 只走本地 / 只走主引擎),一套策略从"选哪个引擎"一路管到"能不能连这个 socket"。第二件是数据回流——研究命中的来源可以下载、抽文本、做嵌入、进私有库,再作为一个本地搜索引擎回头喂给研究。第三件是自动化订阅——把一个主题订阅下来,后台定时跑研究、AI 过滤汇总、投递到新闻流。
本章聚焦这三块,通用检索/合成的主线见 01 / 02 / 04;加密库、设置快照、LLM 提供方等运行时底座见 05。
1. 这一章讲什么(零基础也能懂)
这里不是"通用安全清单",而是三个围着研究核心转的子系统。先把它们各自要解决的问题说清楚。
(1) 出站管制(egress control)——把研究"关"在一个范围里。 LDR 能接很多搜索引擎和 LLM,有的走公网(Google、arXiv、OpenAI),有的在你自己机器上(本地知识库、Ollama)。有人做研究时想要一条硬规矩:"这次研究,数据不许离开我这台机器",或者反过来"只准走公网、别碰我的私有库"。出站管制就是把这条规矩翻译成代码,并且在多个层面反复检查,防止某个新写的、没人复核过的代码路径偷偷把查询发出去。
(2) 纵深防御(defense in depth)一览——其余安全护栏。 除了出站,还有一圈更常规的护栏:防 SSRF(伪造请求打内网/云元数据)、日志脱敏(别把密码写进日志)、账户锁定(防暴力破解)、文件完整性(FAISS 索引被人动过就报警)、只允许白名单模块被动态加载。这些各自独立,本章只做一览 + 点出精华。
(3) 知识库(Library)+ (4) 新闻订阅——两个增值子系统。 研究跑完会命中一堆网页/论文。知识库子系统把它们下载→抽文本→切块嵌入→建 FAISS 索引,变成一个你私有的、可搜索的语料库,还能作为一个"本地搜索引擎"回流给下一次研究用。新闻订阅子系统则把"一个主题"变成"定时自动跑的研究",AI 过滤汇总后投递到新闻流。
(5) 期刊质量评分是个更小的数据子系统,只在本章末尾一句话定位(它的过滤器已在 02 出场)。
2. 顶层全景(它大概怎么转)
先看这三块怎么围着"一次研究运行"咬合。怎么读这张图: 中间竖轴是一次研究的生命周期;左边是套在它外面的安全边界,右边是它产出的数据去了哪、以及订阅怎么反过来驱动它。
安全边界(第3、4节) 一次研究运行 数据回流 & 自动化(第5、6节)
┌───────────────────────────┐ ┌────────────────────────┐
│ 出站策略 PDP(policy.py) │ │ │
│ · 5 种 EgressScope │◄─────│ 选引擎 / 选 LLM / │
│ · evaluate_engine/url/ │ 咨询 │ 选嵌入 / 取 URL │
│ llm/embeddings/retriever│ │ (PEP 调用点) │
└───────────────────────────┘ │ │
┌───────────────────────────┐ │ │ ┌──────────────────────┐
│ 二道防线:PEP-578 审计钩子 │ │ 搜到一批来源 ─────────┼───────►│ 知识库 Library │
│ 拦每一个 socket.connect │◄─────│ │ 下载 │ 下载→抽文本→嵌入→FAISS│
│ (audit_hook.py) │ 兜底 │ │ │ 变成"本地搜索引擎" │
└───────────────────────────┘ │ 合成报告 ◄────────────┼────────┤ 回流给下次研究(引擎) │
┌───────────────────────────┐ │ │ └──────────────────────┘
│ 常规护栏(第4节) │ └───────────▲────────────┘ ┌──────────────────────┐
│ SSRF · 日志脱敏 · 账户锁定 │ │ 定时触发 │ 新闻订阅 News │
│ 文件完整性 · 模块白名单 │ └───────────────────────┤ 订阅→调度→AI 过滤汇总 │
└───────────────────────────┘ │ →投递新闻流 │
└──────────────────────┘
各部件一句话职责:
| 部件 | 干什么 | 主文件 |
|---|---|---|
| 出站策略 PDP | 判定"这个引擎/URL/LLM/嵌入/取回该不该放行" | security/egress/policy.py |
| 审计钩子(二道防线) | 进程级拦截每个 socket.connect,兜住漏网路径 | security/egress/audit_hook.py |
| 保存时校验 | 设置保存时拒绝"把公网主机伪装成本地" | security/egress/validators.py |
| SSRF 校验器 | 拦内网/云元数据 IP、解析差异攻击 | security/ssrf_validator.py |
| 日志脱敏 | 把密码/token 从日志和错误里抹掉 | security/log_sanitizer.py |
| 模块白名单 | 只允许已知搜索引擎类被动态导入 | security/module_whitelist.py |
| 文件完整性 | FAISS 索引被篡改就报警 + 安全反序列化 | security/file_integrity/、research_library/services/faiss_safe_load.py |
| 知识库服务 | 下载/抽取/索引研究来源,建可搜私有库 | research_library/services/、research_library/downloaders/ |
| 库搜索引擎 | 把私有库当搜索引擎回流研究 | web_search_engines/engines/search_engine_library.py、..._collection.py |
| 新闻调度器 | 定时跑订阅研究、后台索引 | scheduler/background.py、news/subscription_runner.py |
3. 出站管制:两道防线怎么咬合
这是本章工程含量最高的一支。先建立最重要的一个直觉。
3.1 一个核心直觉:主防线 + 二道防线
出站管制不是一个函数,而是两层:
- 主防线 = 显式 PEP(Policy Enforcement Point,策略执行点)。 每个已知的出口——建搜索引擎、建 LLM、建嵌入、取一个 URL——都在动手前先问一句策略。这是"正门检票"。
- 二道防线 = PEP-578 审计钩子。 万一有个代码路径没接主防线(新贡献者直接
requests.get、某个 MCP 工具自己开连接、prompt 注入把工具带偏成裸 HTTP),二道防线在最底层的socket.connect上再拦一次。这是"翻墙也会被最外围围栏挡住"。
术语借自零信任 / XACML(security/egress/policy.py:1-15 顶部注释):
| 缩写 | 全称 | 在这里是谁 |
|---|---|---|
| PDP | Policy Decision Point(策略决策点) | policy.py 里的 evaluate_* 函数——只判断,不执行 |
| PEP | Policy Enforcement Point(策略执行点) | 各调用点:工厂建引擎、get_llm、DownloadService 取 URL…… |
诚实边界(源码原话): 这是"进程内的正确性护栏,不是硬安全边界"(policy.py:1-10)。它防的是配置失误、prompt 注入诱导的取 URL、意外出站;它防不住能在 LDR 进程里执行代码的对手——那种对手可以直接 clear_active_context() 把钩子关掉。要硬边界,得在操作系统层叠(network namespace、防火墙、受限 Docker)——audit_hook.py:17-28 的威胁模型讲得很清楚。
3.2 五种出站范围(EgressScope)
用户声明的边界只有五个取值(policy.py:60-91,EgressScope):
| Scope | 含义 | 效果 |
|---|---|---|
STRICT | 只用主引擎,零扩展 | 只有主引擎放行;URL 只允许私有主机 |
PUBLIC_ONLY | 只走公网引擎 | 任何公网引擎/公网 URL 放行 |
PRIVATE_ONLY | 只走本地引擎 | 只放行本地引擎/私有主机,并强制本地 LLM + 本地嵌入 |
BOTH | 任何已分类引擎都行 | 保留策略引入前的行为 |
ADAPTIVE | 跟随主引擎 | 默认值;运行开始时解析成上面某个具体 scope |
两个关键设计点:
-
默认是 ADAPTIVE,不是 BOTH。 大多数人从不碰这个设置,ADAPTIVE 的意思是"跟我主引擎走":主引擎是本地私有库 → 自动变
PRIVATE_ONLY;是公网引擎 → 变PUBLIC_ONLY;分不清 → 退回BOTH(_resolve_adaptive_scope,policy.py:1143)。解析在运行开始时一次性做完,存进 context 的是解析后的具体 scope,不是 ADAPTIVE(context_from_snapshot:1281-1288)。 -
PRIVATE_ONLY 会连带锁死推理路径。 "我的数据留在本机"这个承诺,只有当 LLM 和嵌入也在本地时才成立——云 LLM 会收到查询+检索到的本地片段,云嵌入会在建库时收到整个语料。所以 PRIVATE_ONLY 下
require_local_llm/require_local_embeddings被强制置 True,哪怕用户没开(context_from_snapshot:1290-1305)。STRICT 故意不这么耦合(它只管搜索引擎集,和推理放哪正交)。
ADAPTIVE 解析的分支很直白:
主引擎是…… ADAPTIVE 解析成……
├─ 明确的本地引擎 ───► PRIVATE_ONLY (is_local=True, is_public≠True)
├─ 明确的公网引擎 ───► PUBLIC_ONLY (is_public=True, is_local≠True)
├─ 注册的本地取回器 ──► PRIVATE_ONLY (查 retriever registry)
└─ 分不清 / 出错 ───► BOTH (宽松兜底,永不硬失败一次运行)
3.3 一次运行怎么"武装"二道防线
审计钩子在导入 security 包时就装好了,而且装了就拆不掉(PEP 578 的设计,audit_hook.py:152 install_audit_hook + security/__init__.py:117 调用)。但它默认是睡着的——只有当前线程调用了 set_active_context(ctx) 才对这个线程生效(audit_hook.py:73)。这是刻意的:随便 import 这个包的脚本、pytest 收集器碰个 socket,都不该突然 PolicyDeniedError。
于是每条运行入口都要在开跑前"武装"、跑完"解除":
研究运行开始
│
├─ 已经有人武装了吗?(get_active_context) ← Web worker 在 research_service 里先武装了
│ 有 → 什么都不做(不是我武装的,不归我拆)
│ 没 → 从设置快照 build 一个 EgressContext,set_active_context(ctx)
│
├─ 跑完整研究管线(期间每个 socket.connect 都过钩子)
│
└─ finally: 只有"我武装的"才 clear_active_context() ← 防止线程池复用把 context 泄漏给下一个任务
这段逻辑在 search_system.py:_arm_egress_backstop(351)+ analyze_topic(305,finally 里 336/343 条件清理)。注释点明了为什么要有它:Web worker 会自己武装,但 CLI、新闻调度器、编程式 API 直接构造 AdvancedSearchSystem,不兜一下就会"整条管线在二道防线关闭的状态下跑"(search_system.py:331-335)。
嵌套安全也想到了:active_egress_context 上下文管理器保存/恢复上一个 context,而不是无脑清空(audit_hook.py:120-141)——不然"研究里套聊天"这种嵌套会在内层退出时把外层的 context 也擦掉,悄悄卸掉二道防线。