local-deep-research 出口安全重构基于 Sensitivity × Exposure 双轴数据分类的 DLP 决策模型ADR-0007 深度解读【免费下载链接】local-deep-research~95% on SimpleQA (e.g. Qwen3.6-27B on a 3090). Supports all local and cloud LLMs (llama.cpp, Ollama, Google, ...). 10 search engines - arXiv, PubMed, your private documents. Everything Local Encrypted.项目地址: https://gitcode.com/GitHub_Trending/lo/local-deep-research本篇技术指南系统讲解 local-deep-research 中出口安全护栏egress guardrail的核心架构决策如何把原来单轴的egress_scope策略重构为Sensitivity敏感度× Exposure暴露度两个正交轴的数据分类模型。文中将结合 ADR-0007 原文、egress 包 README 与 classification.py 等源码实现完整给出四象限组合规则、evaluate_run判定算法、Enforcing/Permissive 实施模式、旧模型迁移映射与分阶段落地路线。读完你将掌握如何用两个标签描述一个组件引擎/集合/LLM/嵌入模型的进出风险、为什么敏感数据绝不能到达暴露型出口是唯一核心不变量以及该模型在代码中的真实判定流程与测试真值表。一、为什么需要重构单轴模型的三个根本缺陷重构之前出口护栏位于src/local_deep_research/security/egress/只在单一维度上表达策略每次运行有一个policy.egress_scope枚举adaptive/both/public_only/private_only/strict辅以每个集合的is_public标志与两个require_local推理开关。这套模型能覆盖常见场景但把两个本质独立的属性压缩进了一个词里具体暴露出三个问题问题 1敏感度与暴露度是两回事却被融进同一个词。public ↔ private 被视为一条光谱但其中隐藏着两个截然不同的问题数据是否敏感——这是**来源source**的属性一个集合、一个文档库。目的地是否把数据向外暴露——这是**出口sink**的属性你把查询发过去的网络搜索引擎你把文本块发过去的云端 LLM。最典型的症状是集合的is_public标志这个词暗示数据被公开发布但集合永远是本地存储——检索它绝不会把其内容推给搜索引擎。is_public真正授权的是云推理即public用暴露轴的语言夸大了敏感轴的含义。问题 2双重风险来源无法表达。Elasticsearch 与 Paperless 直接击穿了单轴模型。一个 Elasticsearch 实例可能同时是敏感数据存储你的私有语料→ 绝不能与暴露型出口组合以及/或者受监控/外部服务一个本身会暴露你查询的出口。这是两种相反的风险单一的is_local/is_public标志只能二选一。在当前模型下对这些引擎只能依赖不对称的 URL fail-up 保护文档明确承认cannot guarantee anything无法保证任何东西因此现阶段不应自动与其他来源组合。问题 3both是静默的 blanket 许可。both一次性覆盖所有来源的逐项分类且没有任何可见或概念上的信号提示保护已被撤销——它是逐来源分类与关掉策略之间一团模糊的中间地带。二、两轴模型Sensitivity × Exposure重构后的核心是把每个运行会触碰的模型搜索引擎、集合/存储、LLM、嵌入模型在两个正交轴上打标签。完整矩阵如下语义具有某敏感度行× 某暴露度列的组件应如何处理Contained 出口Exposing 出口Non-sensitive 来源可与任何东西组合——公共集合、本地 Ollama只能与non-sensitive 来源组合——公共网络引擎、云端 LLMSensitive 来源只能与其他contained来源组合绝不能是 exposing 出口——私有集合 本地 LLM仅限单独使用配 contained本地推理——存放私有数据的远程受监控存储一句话规则一次运行绝不能让 sensitive 来源到达 exposing 出口——除非显式切换到Permissive模式该模式只警告、从不拦截。两个轴的正式定义轴适用对象取值对应问题Sensitivity来源 sourcessensitive/non-sensitive这些数据是否绝不能离开本机Exposure出口 sinksexposing/contained把数据发到这里是否使其离开本机值得强调的是某些组件同时扮演两种角色——搜索引擎既是你查询的sink又是结果的sourceElasticsearch 既是数据的source又是你查询的sink。因此每个组件在它参与的每个轴上都要有一个标签这正是单轴模型无法表达的、本次变更的核心。2.1 该模型与业界标准的渊源双轴框架就是标准的DLP / 信息流控制information-flow control模型按敏感度给数据分类、按信任/暴露度给目的地分类、禁止敏感→暴露的流动。实施模式的词汇借用自SELinuxenforcing/permissive/disabled。egress 包原本就借用了 XACML / 零信任的 PDP–PEP 词汇因此这是延续而非外来引入。2.2 与查询文本无关设计边界该模型分类的是来源与出口不是用户输入的查询问题。运行查询会原样发送给运行所使用的各个搜索出口因此在暴露型引擎上输入敏感问题无论如何标注来源数据都会离开机器。护栏无法检查或净化查询意图——这始终是用户的责任。UI 必须明确说明我们无法保护你的问题内容请根据问题敏感度选择合适的来源。三、逐组件分类每个组件都携带两个标签分类是按组件计算的组件指运行触碰的每一个模型每个搜索引擎、每个集合/存储、LLM 与嵌入模型。ADR 设想的统一入口是单一函数classify(component, settings) - (Sensitivity, Exposure)成为今天分散在classify_engine、_CLOUD_LLM_PROVIDERS集合、逐集合is_public查询与 URL fail-up 中的共享逻辑的唯一归属。ADR 给出的示例分类表组件示例SensitivityExposure公共集合 / 公共网络结果non-sensitivecontained私有集合默认sensitivecontained公共网络 / 学术引擎Google、arXivnon-sensitiveexposing查询出口云端 LLM / 嵌入模型Anthropic、OpenAInon-sensitiveexposing本地 LLM / 嵌入模型Ollama、sentence-transformersnon-sensitivecontainedPaperless / Elasticsearch — 本地、私有sensitivecontainedElasticsearch — 远程 / 受监控、私有sensitiveexposing源码佐证这些标签在引擎类上是显式声明的类属性。例如 search_engine_arxiv.py 声明egress_sensitivity Sensitivity.NON_SENSITIVE、egress_exposure Exposure.EXPOSINGsearch_engine_elasticsearch.py 声明egress_sensitivity Sensitivity.SENSITIVE、egress_exposure Exposure.CONTAINED。同样被标注的还有 Brave、DuckDuckGo、Exa、GitHub 等公共引擎均为NON_SENSITIVE EXPOSING。Sensitivity/Exposure枚举本身定义在 classification.py 中。3.1 角色Role一个组件可以多重参与由于搜索引擎既收查询又产结果classification.py 定义了三种参与角色SOURCE—— 向运行贡献数据SEARCH_SINK—— 接收查询以及可能的扩展结果INFERENCE_SINK—— LLM / 嵌入模型能看到全部来源数据。同一个真实组件会被按它扮演的每个角色分别传给evaluate_run每次使用相同的Component.name——第四象限双重风险存储单独使用的自我排除机制正是依赖这一共享身份。四、四象限组合规则与判定算法一次运行触碰的是一组组件集合是否被允许直接由两个标签推导#一个组件是…可与以下内容组合示例1non-sensitive, contained任何东西公共集合本地 Ollama2sensitive, contained仅其他contained来源敏感与否均可——不得有 exposing 出口私有集合 本地 LLM3non-sensitive,exposing仅non-sensitive来源云端 LLM公共网络引擎4sensitive exposing唯一的 sensitive 来源non-sensitive contained 同伴可以配contained本地推理存放私有数据的远程受监控存储其底层不变量是运行不得让 sensitive 来源到达 exposing 出口。由于智能体agentic运行能把一个引擎的结果变成另一个引擎的查询即威胁模型中的 LangGraph 静默扩展类问题该规则是针对整次运行的组件集合执行的而不是逐次调用。等价表述一次运行必须要么全 non-sensitive要么不含 exposing 出口——唯一的例外是允许一个孤立的第四象限组件作为唯一 sensitive 来源运行non-sensitive contained 同伴仍可因为把自身数据返回给自身并不是新的泄露且其推理被强制 contained。4.1evaluate_run四象限的真实判定逻辑纯决策核心实现在 classification.py 的 evaluate_run模式无关的_decide_enforcing在 L131-L187。算法分三步收集敏感来源汇总所有Role.SOURCE且 sensitivity 为SENSITIVE的组件名若为空no_sensitive_source任何出口都无所谓直接放行——这对应第一/三象限。检查推理出口sensitive_to_exposing_inferenceLLM/嵌入模型能看见每个来源的数据因此任何EXPOSING的INFERENCE_SINK都会泄露敏感内容立即拒绝。检查搜索出口sensitive_to_exposing_search一个 exposing 的SEARCH_SINK只有在它自身就是唯一敏感来源第四象限单独使用时才允许若存在其他敏感来源可能被智能体扩展成发往该出口的查询或该出口自身不敏感防止名称巧合被误认为单独双风险场景则拒绝。拒绝时返回Decision(allowedFalse, reason, offending)其中reason是简短机器码如sensitive_to_exposing_inferenceoffending列出违规组件名。推理出口检查优先于搜索出口检查对应测试test_multi_violation_inference_takes_precedence。4.2 测试真值表规则被钉死test_egress_classification.py 将四象限规则完整钉死为单元测试真值表阶段 A 的纯核心在接入 PEP 前即被测试锁定后续接线无法悄然改变行为关键断言包括全 non-sensitive 运行允许任意出口no_sensitive_source私有集合 本地 Ollama 允许sensitive_contained私有集合 Anthropic 拒绝且offending (llm:anthropic,)私有集合 arXiv 公共引擎拒绝且offending (arxiv,)双风险存储Elasticsearch单独 本地推理允许配云推理、配其他敏感来源、或两个双风险存储并存均拒绝暴露型嵌入模型embeddings:openai与敏感来源同现拒绝Permissive 模式下所有被拒组合均放行但保留permissive:reason前缀的诊断供 UI 横幅使用空集合允许名称巧合不能豁免非敏感出口test_name_collision_does_not_exempt_non_sensitive_sink。五、实施模式SELinux 词汇与 UNPROTECTED 逃生舱classification.py 定义了两种实施模式Enforcing—— 当前行为执行不变量违规引擎/出口被拦截。PermissiveUnprotected—— 逃生舱策略仍被评估因此警告横幅照常触发但什么都不拦截。取代both配以诚实、醒目、浅红色的 UI。用户可见文案待定Unprotected / Unrestricted / Permissive。逃生舱在evaluate_run中的实现很讲究Mode.PERMISSIVE下运行总是被允许但底层本应被执行的reason/offending会被保留以permissive:前缀以便调用方仍能弹出警告横幅见 classification.py L118-L128。在policy.py中EgressScope.UNPROTECTED是运算符启用的逃生舱默认关闭对应环境变量LDR_POLICY_ALLOW_UNPROTECTED_EGRESStrue。启用时出口范围限制被禁用任何引擎/URL/供应商都允许但evaluate_url中的硬性 SSRF 云元数据拦截仍然生效且强制本地推理的要求被解除。激活期间会显示不可关闭的横幅。迁移00270027_disable_legacy_unprotected_egress.py会把遗留的已存储/已排队unprotected值一次性改写成adaptive防止后续选择静默重新激活。六、旧模型到双轴的映射迁移而非重写原单轴模型的每个概念都被映射到新轴上见 policy.py 中EgressScope的注释说明集合is_publicFalse→ Sensitivitysensitiveis_publicTrue→non-sensitive从public重新标签——它从不发布。公共网络 / 学术引擎 → Exposureexposing。云端 LLM / 云端嵌入模型 → Exposureexposing出口require_local_*意为禁止暴露型推理出口。本地集合 / 本地 LLMOllama→ Exposurecontained。各 scope 重新表达private_only≈ 无 exposing 出口public_only≈ 允许 exposing 出口排除 sensitive 来源strict≈ 单一来源adaptive≈ 从主引擎推断运行姿态。both→移除由逐来源分类 Permissive 模式取代。Elasticsearch / Paperless → 默认声明为sensitive contained可用于第四象限以外的其他 contained/本地来源组合当配置的端点解析为公共主机时URL fail-up 把暴露度翻转为exposing第四象限。按目的地的信任条目policy.trusted_search_engines可以把恰好位于公共主机名上的自托管实例重新标回 contained。关于both的退役细节policy.py L70-L75EgressScope.BOTH仍作为枚举值存在但已不再是用户可选择的 scope——它只是adaptive解析到不可分类主引擎时的内部解析结果。任何残留的用户both值存储/环境变量/队列都会被context_from_snapshot强制改写为adaptive数据库行由迁移00190019_retire_both_egress_scope.py重写。USER_SELECTABLE_PROTECTED_SCOPESpolicy.py L95-L102只保留adaptive/public_only/private_only/strict四个受保护范围BOTH刻意缺席。6.1 动态细化运行时解析纯核心之上的运行时解析器是 run_classification.py。engine_label()L45-L107读取引擎类声明的标签并应用三个依赖配置的动态细化URL fail-up自托管存储的配置 URL 解析为公共主机时exposure 翻转为EXPOSING复用策略的不对称 URL 覆盖只会收紧不会放松集合敏感度翻转标记为 public 的集合 →NON_SENSITIVE聚合的library始终保持 sensitive_resolve_collection_is_public对其硬返回私有向量存储暴露当索引所在的向量存储不是本地文件如 FAISS 的is_local_fileTrue且端点不解析为本地时集合的 exposure 翻转为EXPOSING——同样的 fail-up 应用到 RAG 出口上。推理出口LLM/嵌入模型的标签由_inference_label()L142-L165解析与强制实施的 PEP 使用完全相同的分类在 require-local 探测上下文中调用evaluate_llm_endpoint/evaluate_embeddings使解析器与实施保持精确同步——自托管本地 URL 端点判为 contained而非误判 exposing。classify_run()L190-L232组装运行组件集合每个搜索引擎贡献一对同名组件SOURCESEARCH_SINKLLM 与嵌入模型各贡献一个INFERENCE_SINK。未知引擎不在注册表中fail closed 到最严格的第四象限SENSITIVE EXPOSING确保未分类来源永远不会悄然放宽运行的可准入性。七、按目的地信任我信任 Anthropic场景这是用户管理的覆盖项把特定出口重新标记为contained例如零保留的 Anthropic 端点、位于公共主机名上的自托管 Paperless——即暴露轴上的集合提升对应物。落地形式为两个 JSON 列表设置policy.trusted_inference_providers—— 信任的推理供应商把暴露型 LLM/嵌入出口放松为 containedpolicy.trusted_search_engines—— 信任的搜索引擎重新包含因公共 URL 而 fail-up 的双风险存储从第四象限移回第二象限。run_classification.py L99-L105 中的关键安全约束信任只对因 URL fail-up 的本地性质存储生效绝不会应用于天生公共的引擎is_public——受信任的名字不能洗白一个公共搜索出口。设置保存时由 validators.py 的 validate_trusted_search_engines 校验拒绝把天生公共的引擎写入信任列表。信任生效后会有专门的信任横幅提示用户。八、落地路线图核心优先、测试网保护下的增量重写出口护栏是安全关键组件已经历过两轮对抗性评审。大爆炸式重写爆炸半径太大因此 ADR 明确选择核心优先、每阶段保持 egress 测试套件全绿的增量路线阶段 A —— 分类核心security/egress/classification.pySensitivity/Exposure类型、classify(component, settings)、实现四象限规则的evaluate_run(components)——纯函数、对照上述真值表完全单元测试、尚未接线零行为变更。阶段 B —— 接入 PEP把各执行点接到核心上用新模型词汇重新表达既有 scopes保持行为对等既有 egress 测试继续全绿同时新能力第四象限、双风险来源变为可达。阶段 C —— UI呈现两个轴、增加Permissive / Unprotected模式、移除both、让集合标签诚实化。阶段 D —— 按目的地信任暴露轴覆盖项即信任 Anthropic场景与 Elasticsearch / Paperless 的显式分类。当前状态PR #4882四个阶段全部实现。A / B—— 分类核心classification.py 解析器run_classification.py 每个引擎与供应商上的显式逐元素标签。C——UNPROTECTED逃生舱SSRF / 云元数据不变量在逃生舱门之后仍保留、both彻底退役从选择器中移除存量行由迁移 0019 改写为adaptive任何残留值——环境变量/排队快照/未迁移数据库——在读取时强制改写为adaptiveEgressScope.BOTH仅作为不可分类 ADAPTIVE 主引擎的内部解析结果存续新增不可关闭的保护已禁用横幅以及实施翻转运行启动预检把双轴拒绝作为 scope PEP 之上的纵深防御UNPROTECTED按 permissive 评估不可计算的决策fail closed——返回拒绝性audit_error——而不是 open。该预检同时运行在 Web/api/start_research预检和run_research_process的共享 worker 咽喉点因此 follow-up / chat / queue 运行都被覆盖只有绕过两者的 CLI / 程序化调用方仅受 scope PEP 保护在private_only/ adaptive-private 下仍强制本地推理。D—— 按目的地信任policy.trusted_inference_providers/policy.trusted_search_engines把受信任的离机出口放松为 contained、信任横幅、诚实的集合标签文案。在发布版本中开始强制实施前必须通过新一轮对抗性评审——实施翻转是对安全边界的既有行为变更。九、已知残留问题跟踪于 issue #4951四轮对抗性评审确认运行启动审计是尽力而为的纵深防御而非完备性保证——它从设置重新推导每个出口的分类可能与运行实际连接的对象产生偏差。默认adaptive配置受 scope PEP 保护私有主引擎强制本地推理退役both消除了审计是唯一守卫的 scope。以下边缘被记录为 issue 而非阻塞项搜索轴 —— Elasticsearchcloud_id暴露 fail-up 检查的是hosts而非cloud_idpermissive scope 下通过cloud_id可达的敏感 ES 存储被分类为 contained。引擎自带的_cloud_id_forbidden_by_scope覆盖private_only/strict。完整引擎集合 / 智能体扩展强制实施审计和 scope PEP 的本地推理耦合以运行的主引擎为键运行中途扩展拉入的非主敏感引擎不会回映到require_local。公共主机上的双风险存储URL 解析为公共的自托管存储被分类为 PUBLIC_ONLYscope 级本地推理耦合不触发Web 路径上的运行启动审计仍会标记。按名称键控的信任漂移trusted_inference_providers按供应商名称而非经核验的端点 URL 键控且仅在审计中生效、不在边界 PEP 中生效。LibraryRAGService直接构造从全局search.tool解析其主引擎直接非工厂构造可能漏掉本地嵌入耦合。查询文本按设计不在范围内见上文。十、开放问题的最终决议延后 —— 用户可见轴词汇。内部已定为Sensitivity{sensitive, non_sensitive}/Exposure{contained, exposing}。两个轴尚未作为独立 UI 控件呈现——目前通过既有 egress-scope 选择器、逐集合 public 标志与信任列表表达。专用双轴 UI 是后续工作。已解决 —— Unprotected。设置值 / 标签 /EgressScope.UNPROTECTED内部实施模式另为Mode.PERMISSIVE。已解决。Elasticsearch / Paperless 默认sensitive contained公共端点上的 URL fail-up 把暴露度翻转为 exposing第四象限policy.trusted_search_engines是显式覆盖项。已解决 —— 否。聚合library始终敏感_resolve_collection_is_public对其硬返回私有。已解决 —— 设置列表。policy.trusted_inference_providers/policy.trusted_search_enginesJSON 列表设置镜像allowed_local_hostnames的形态不建独立数据表。延伸阅读egress 包 README —— 完整背景PDP–PEP 架构、全部 PEP 落点表、设置键、威胁模型与数据流图。classification.py —— 纯决策核心阶段 A。run_classification.py —— 运行时解析 审计/实施接线阶段 B。test_egress_classification.py —— 四象限规则真值表测试test_egress_policy.py 与 test_egress_run_classification.py 覆盖 scope 解析与运行分类。迁移0019_retire_both_egress_scope.py、0027_disable_legacy_unprotected_egress.py。【免费下载链接】local-deep-research~95% on SimpleQA (e.g. Qwen3.6-27B on a 3090). Supports all local and cloud LLMs (llama.cpp, Ollama, Google, ...). 10 search engines - arXiv, PubMed, your private documents. Everything Local Encrypted.项目地址: https://gitcode.com/GitHub_Trending/lo/local-deep-research创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
