DeerFlow 授权体系 Phase 1A:可信 Principal 身份链路与内置 RBAC 核心实现指南
本文基于 DeerFlow 仓库中的实施计划文档 2026-07-15-authz-phase1a-implementation-plan.md 展开,系统讲解 Phase 1A「可信身份链路与内置 RBAC 核心」的设计目标、固定架构、全局语义锁,以及两个可独立合并 PR 的完整实施步骤(含 TDD 流程、验收标准与检查命令)。读完本文,你将掌握:如何从 Gateway 服务端认证状态构建不可伪造的授权身份(Principal)、如何在 subagent 与 Guardrail 链路中完整传递身份字段,以及如何实现一个严格、确定性的内置 RBAC provider 与统一的 provider factory——并能在当前仓库中对照源码逐条验证这些设计决策。
1. 背景与目标:Phase 1A 只建核心,不接执行路径
该计划是 DeerFlow 可插拔授权(Pluggable Authorization)整体方案的中间阶段,上游依据为:
- RFC 设计:2026-07-10-pluggable-authorization-rfc.md
- 前情记录:2026-07-10-pluggable-authorization-implementation-notes.md
- Phase 0 基线:PR #4127 / 提交
1300c6d3
Phase 1A 的边界非常明确:只建立身份和策略核心,不把授权接入工具装配或执行路径。 为降低 review 复杂度,计划将其拆分为两个可独立合并的 PR:
- Phase 1A-1:可信 Principal 链路
- 从 Gateway 的服务端认证状态产生
is_internal。 - 使用唯一的 Principal builder 解析身份。
- 将完整身份传递到 subagent 和 Guardrail adapter。
- 从 Gateway 的服务端认证状态产生
- Phase 1A-2:内置 RBAC 与 provider factory
- 实现严格、确定性的内置 RBAC provider。
- 实现统一的 provider 实例化和 Protocol 校验。
Phase 1A 合并后的准确兼容性承诺是(这一点在实施时尤其重要,写 PR 描述时应原样复述):
authorization.enabled: false时,工具集合和工具执行决策不变。- runtime context 会新增服务端生成的身份字段,这是有意的可观察变化。
- Phase 1A 不执行 Layer 1 过滤,也不自动安装 Layer 2 middleware。
2. 固定架构与职责划分
计划文档首先锁定了整条身份链路的数据流向:
Gateway 认证状态
│
├─ 清除客户端伪造的服务端身份字段
├─ 注入 user_id / user_role / oauth_* / is_internal
▼
runtime context
│
├─ build_principal_from_context(default_role)
│ └─ Phase 1B Layer 1 使用
│
├─ task tool → SubagentExecutor → subagent runtime context
│
└─ GuardrailMiddleware → GuardrailRequest
→ GuardrailAuthorizationAdapter
→ build_principal_from_context(default_role)
→ AuthorizationProvider
与架构配套的是职责单一原则,每个组件只做一件事:
- Gateway:确认字段来源可信,覆盖或清除客户端值。
- Principal builder:补齐缺失角色、规范化类型、复制 attributes。
- RBAC provider:只解释已经解析好的 Principal 和角色策略。
- provider factory:加载、构造、校验 provider,不执行授权策略。
- GuardrailMiddleware:处理 provider 异常以及
fail_closed。 - adapter:只做数据映射和决策类型转换,不捕获异常。
这套职责边界在当前仓库源码中可以得到逐条印证,例如 adapter 的文档字符串明确声明其职责(见 adapter.py 开头注释:「只做数据映射和决策类型转换」,且 evaluate/aevaluate 有意让 provider 异常向上抛,交给 middleware 的 fail_closed 语义统一处理,见 adapter.py)。
3. 全局语义锁
计划文档第 3 节称为「全局语义锁」——这是 Phase 1A 中最核心、最容易被实现者随意发挥的部分,因此用规则逐条锁死。
3.1 Principal 语义
role仅在user_role为None或空字符串时使用default_role。- 未知但非空的角色不能回退到
default_role(这一点直接关联 fail-closed 语义,见 3.4)。 is_internal只有原值严格等于True时才为True。authz_attributes必须是Mapping;其他非空类型抛TypeError。attributes总是复制为新字典,不共享输入引用。- Layer 1 和 Layer 2 必须通过同一个 builder 获得语义一致的 Principal。
源码实现 principal.py 与上述规则完全对应:resolved_role 仅在 None 或 "" 时回退;is_internal=context.get("is_internal") is True 是严格的布尔判定;normalize_authz_attributes()(principal.py)对非 Mapping 值抛 TypeError,并对合法值执行 dict(raw) 深一层复制。文件头注释也强调它是「the single sanctioned way to construct a Principal」,是纯函数:不读全局 AppConfig、不缓存、不修改输入。
3.2 服务端身份字段(防伪造)
is_internal、authz_attributes 和 channel_user_id 是服务端拥有字段,规则如下:
- Gateway 必须从
config.context和config.configurable两个 section 清除客户端值。 is_internal只能由request.state.auth_source == AUTH_SOURCE_INTERNAL产生。channel_user_id只能来自内部认证 IM 调用方的顶层body.context;普通 session 请求和body.config两个 section 中的同名值必须删除。- 写入使用直接赋值,不能使用
setdefault(否则客户端值可能先占位)。 - 写入必须发生在
user_id is None等所有 early return 之前。 - Phase 1A 尚无 Gateway 侧
authz_attributes权威生产者,因此 Gateway 路径固定为空;不能接受普通 HTTP 客户端提供的 attributes。 - 嵌入式 Python 调用属于进程内可信调用,可直接使用 builder 构造 attributes。
在 Gateway 的实际实现 services.py 中可以看到这套逻辑的落点:inject_authenticated_user_context() 开头先对 context 和 configurable 两个 section 弹出服务端字段(实现中的常量名为 _SERVER_OWNED_RUNTIME_CONTEXT_KEYS,计划文档给出的 _SERVER_OWNED_AUTHZ_CONTEXT_KEYS 是示意性命名),随后用直接赋值写入 runtime_context["is_internal"] = auth_source == AUTH_SOURCE_INTERNAL——这一步位于 user_id is None 的 early return 之前,保证即使无登录用户的路径也有确定的 is_internal 值。channel_user_id 只在 auth_source == AUTH_SOURCE_INTERNAL 且顶层 request_context 携带时才写入。此外从源码注释可见,实现还进一步把 user_id 也纳入了外部调用方的服务端所有权清洗(PR #3294 相关),这比 Phase 1A 计划更强,属于后续阶段的演进。
3.3 RBAC 策略语义
每个角色的资源策略使用以下语义(这是 RBAC provider 的完整行为契约,实现与测试都必须严格对齐这张表):
| 配置 | 行为 |
|---|---|
allow: "*" 或 allow: true |
默认允许全部候选 |
allow: [...] |
只允许列表成员 |
allow: [] 或 allow: false |
全部拒绝 |
allow 缺失 |
默认允许,仍应用 deny |
deny: [...] |
从允许集合移除;deny 永远优先 |
| 资源配置缺失 | 此 provider 不限制该资源 |
| Principal 角色缺失 | 视为 builder/调用方错误,抛明确异常 |
| Principal 角色未知 | 抛明确异常,由执行层按 fail_closed 处理 |
| 配置类型、成员类型或字段非法 | provider 构造时抛 ValueError |
资源名必须显式映射,禁止通过简单加 s 猜测:
RESOURCE_POLICY_KEYS = {
"tool": "tools",
"model": "models",
"skill": "skills",
"sandbox": "sandbox",
"mcp_server": "mcp_servers",
"route": "routes",
}
未知 resource 使用原名查找;未配置时按「资源配置缺失」处理。Phase 1A 的行为测试以 tool -> tools 为主,其他资源只固定映射契约,不接线。实现中该映射位于 rbac.py 的 _RESOURCE_POLICY_KEYS,与文档完全一致;并且实现还追加了一个更严格的约束:配置中若把 tools 等请求端别名反向写成保留的 request 别名(见 rbac.py),会在构造期直接抛 ValueError,防止静默错配。
3.4 fail-closed 边界
fail_closed不传给 provider,也不由 provider 或 adapter 解释。- provider 遇到未知身份或内部错误时抛异常。
- Phase 1B 的 Layer 1 helper 和现有
GuardrailMiddleware分别在边界处应用fail_closed。 - provider 主动返回
allow=True不属于异常,middleware 不会替它改判;因此未知角色绝不能返回 allow——这正是 3.3 中「未知角色抛异常」的原因:只有异常才能触发 fail-closed,静默 allow 会造成安全漏洞。
4. PR Phase 1A-1:可信 Principal 链路
计划建议的 PR 标题:
feat(authz): propagate trusted authorization principal context
Step 1:先写失败测试(TDD 起点)
新增 backend/tests/test_authorization_principal.py,覆盖:
- 空 context 和部分 context。
- 缺失、
None、空字符串角色使用default_role。 - 未知非空角色原样保留。
is_internal仅接受严格布尔True。- attributes 缺失、复制、输入修改不反向影响 Principal。
- attributes 非 Mapping 时抛
TypeError。
扩展 backend/tests/test_gateway_services.py:
- 普通请求通过
body.config.context伪造is_internal=True,最终为False。 - 普通请求通过
body.config.configurable伪造该值,最终被清除。 user=None时仍写入is_internal=False。- 内部认证请求写入
is_internal=True。 - 普通请求注入的
authz_attributes从两个 config section 中被清除。 - 原有 user/owner/oauth 注入测试继续通过。
扩展现有 subagent/guardrail 测试:
is_internal=True/False均能原样传递,不能只在 truthy 时写回。channel_user_id和 attributes 一同传递。- subagent 对 attributes 使用副本。
- adapter 同步、异步路径映射出相同 Principal。
- adapter 通过 builder 应用
default_role,不维护第二套回退逻辑。
上述测试在当前仓库中均已存在:test_authorization_principal.py、test_gateway_services.py、test_authorization_provider.py。
Step 2:实现 Principal builder
新增文件 backend/packages/harness/deerflow/authz/principal.py,接口为:
def build_principal_from_context(
context: Mapping[str, Any],
*,
default_role: str,
) -> Principal:
...
实现保持纯函数,不读取全局 AppConfig,不缓存结果,不修改输入。当前实现见 principal.py,签名与文档一致,且额外导出了共享的 normalize_authz_attributes(),供所有传递点(middleware、executor、task_tool)复用同一归一化入口。
Step 3:保护并注入 Gateway 身份
修改 backend/app/gateway/services.py,新增独立的服务端拥有字段常量(计划文档示意名 _SERVER_OWNED_AUTHZ_CONTEXT_KEYS,实际实现命名为 _SERVER_OWNED_RUNTIME_CONTEXT_KEYS)。在 inject_authenticated_user_context() 开头依次执行:
- 从
context和configurable删除客户端伪造值。 - 确保 runtime context 是字典;若输入类型非法,使用现有配置错误约定明确失败,不能静默保留伪造值(实现中直接
raise TypeError("run context must be a mapping"),见 services.py)。 - 直接写入服务端计算的
is_internal。 - 再执行现有 user/internal owner 分支和 early return。
关键红线:不要把 is_internal 加进允许 internal caller 自定义的普通 override 白名单;它始终由认证中间件产生。
Step 4:完整传递 subagent 身份
修改 backend/packages/harness/deerflow/tools/builtins/task_tool.py 和 backend/packages/harness/deerflow/subagents/executor.py,从 parent runtime context 捕获:
user_id / user_role / oauth_provider / oauth_id / channel_user_id
is_internal / authz_attributes
SubagentExecutor 构造时复制 attributes;写回 subagent context 时再次复制;is_internal 必须无条件写回布尔值(包括 False)。当前 task_tool.py 的实现展示了这一契约:is_internal = parent_context.get("is_internal") is True 执行严格布尔判定,authz_attributes 经 normalize_authz_attributes() 复制,然后与 channel_user_id 一并写入子代理 context。
Step 5:扩展 GuardrailRequest 并复用 builder
修改四个文件:backend/packages/harness/deerflow/guardrails/provider.py、backend/packages/harness/deerflow/guardrails/middleware.py、backend/packages/harness/deerflow/authz/adapter.py、backend/tests/test_authorization_provider.py。
GuardrailRequest 增加向后兼容的默认字段(实际实现见 provider.py,默认值确保旧 provider 不受影响):
channel_user_id: str | None = None
is_internal: bool = False
authz_attributes: dict[str, Any] = field(default_factory=dict)
adapter 构造函数增加 default_role 参数,并在 _to_authz() 中调用 build_principal_from_context(),删除 Phase 0 中「不映射 is_internal」的说明。当前 adapter.py 中 _to_authz() 将 GuardrailRequest 的身份字段整体组装入 context 字典后交给 builder,且默认 default_role="user"、resource_type="tool"、action="call"——同步与异步路径(evaluate/aevaluate)走同一映射,保证决策一致。
Step 6:导出与文档
修改 deerflow/authz/__init__.py 导出 builder(当前 init.py 已导出 build_principal_from_context、normalize_authz_attributes 等)。向 implementation notes 的决策日志追加 Phase 1A-1 记录,不改写 Phase 0 历史。
Phase 1A-1 验收标准
- 删除 Gateway 对
is_internal的直接赋值后,防伪测试必须失败(证明测试真正覆盖了防伪逻辑)。 - 普通请求无法从
body.context或body.config注入channel_user_id,内部认证 IM 请求只保留顶层body.context的 sender id。 - 删除任意一段 subagent 传递后,继承测试必须失败。
- Gateway、subagent、adapter 得到的身份字段一致。
- 没有 Layer 1/Layer 2 自动接线。
- 现有 guardrail、Gateway、subagent 测试全部通过。
5. PR Phase 1A-2:内置 RBAC 与 provider factory
计划建议的 PR 标题:
feat(authz): add built-in RBAC provider and provider factory
前置条件:Phase 1A-1 已合并,或当前分支已 rebase 到其提交。
Step 1:先写失败测试
新增 backend/tests/test_rbac_authorization_provider.py,覆盖:
- wildcard、布尔 allow、列表 allow、空列表、allow 缺失五种形式。
- deny 优先于所有 allow 形式。
- 资源配置缺失时不限制。
tool -> tools等显式资源映射。- 未知角色和缺失角色抛异常,绝不返回 allow。
- 非法 roles、role policy、resource policy、allow/deny 类型和非字符串成员。
authorize()与aauthorize()决策一致。filter_resources()与逐项authorize()结果一致。- 过滤保持 candidates 顺序和重复项,不增加输入中不存在的资源。
- 构造后修改原配置不会改变 provider 行为(配置不可变性)。
新增 backend/tests/test_authorization_runtime.py,覆盖:
- disabled 时直接返回
None,且不尝试 import 无效 class path。 - enabled 但 provider 缺失时抛明确错误。
- 路径不存在、目标不是 class、构造失败时错误包含 class path 并保留异常链。
- 实例不符合
AuthorizationProviderProtocol 时明确失败。 - provider factory 不注入
fail_closed或default_role。 - 内置 RBAC 能通过相同标准路径解析,不写特殊分支。
Step 2:实现 RBAC provider
新增 backend/packages/harness/deerflow/authz/rbac.py,硬性要求:
- 构造时完成全部配置校验和规范化(fail fast)。
- allow/deny 预编译为不可变集合或明确的「全部/全部拒绝」标记。
- 请求路径只做 O(1) membership 和 O(n) candidates 遍历。
- 返回稳定 reason code;拒绝消息包含 role、resource、target,但不包含敏感配置。
filter_resources()保持输入顺序,不修改输入列表。- 不读取全局 config,不处理
fail_closed,不二次应用default_role。
当前实现 rbac.py 展示了这套「构造期编译」策略:RbacAuthorizationProvider.__init__ 将 roles 配置逐一编译为不可变的 _CompiledPolicy(frozenset + 哨兵 _ALL/_ABSENT 区分「未配置 allow」与「allow 为 null」),并在 _compile_resource_policy() 中拒绝未知 policy key(如拼错的 alow),防止静默误授权。is_allowed() 判定顺序固定为:先查 deny,再查 allow 全集,最后做成员判断——deny 永远优先。
Step 3:实现 provider factory
新增 backend/packages/harness/deerflow/authz/runtime.py,接口为:
def resolve_authorization_provider(
config: AuthorizationConfig,
) -> AuthorizationProvider | None:
...
固定执行顺序:
enabled=False:立即返回None。enabled=True且 provider 缺失:抛ValueError。- 使用
resolve_variable(path, expected_type=type)解析 class。 - 只用
provider.config中显式提供的 kwargs 构造实例。 - 使用
isinstance(instance, AuthorizationProvider)做结构校验。 - 包装错误时包含 class path、保留
raise ... from err,不打印 kwargs。
factory 不缓存 provider;Phase 1B 在每次 agent build 时解析一次,并把同一个实例传给 Layer 1 和 Layer 2。从源码看,当前 runtime.py 实际拆成了两阶段 API——resolve_authorization_provider_spec() 负责发现(可 offload 出异步事件循环,可能 import 自定义模块),construct_authorization_provider() 负责在事件循环上构造并校验,同步便捷函数 resolve_authorization_provider() 组合二者。构造阶段还额外校验 default_role 是否已在 RBAC provider 的已知角色中(runtime.py),把配置错误提前到启动期暴露。
Step 4:导出、文档与配置边界
修改 deerflow/authz/__init__.py 导出 RBAC provider 和 factory(当前 init.py 已同时导出 RbacAuthorizationProvider 与 resolve_authorization_provider)。向 implementation notes 追加 Phase 1A-2 决策记录。
关键边界决策:Phase 1A-2 不修改 config.example.yaml——在执行层尚未接线时展示「启用 RBAC」的用户配置会造成已经生效的错觉。完整 RBAC 示例随 Phase 1B enforcement 一起加入,届时同时搜索 Helm values 和相关文档镜像。Phase 1A 不改变配置 schema,因此不 bump config_version。
Phase 1A-2 验收标准
- 未知角色无法静默放行。
- deny 在
authorize和filter_resources中都优先。 - 内置和自定义 provider 走相同 factory 路径。
- disabled 路径不 import、不构造 provider。
- provider 配置在构造后不可被外部可变引用改变。
- 没有 Layer 1/Layer 2 自动接线。
6. 测试与检查命令
每个 PR 按 TDD 顺序执行:先提交/观察失败测试,再实现到通过。计划文档给出的检查命令为:
cd backend
uv run pytest tests/test_authorization_principal.py -q
uv run pytest tests/test_authorization_provider.py tests/test_gateway_services.py -q
uv run pytest tests/test_rbac_authorization_provider.py tests/test_authorization_runtime.py -q
uv run pytest tests/test_harness_boundary.py -q
uv run ruff check packages/harness/deerflow/authz app/gateway/services.py tests
uv run ruff format --check packages/harness/deerflow/authz app/gateway/services.py tests
其中 test_harness_boundary.py 用于守住 deerflow 包与 app 层之间的模块边界(Phase 1A 新增的 authz 模块位于 harness 内,不能被 app 层反向依赖)。提交前再运行 make test;如果全量测试受环境依赖阻塞,PR 描述必须列出已运行命令、通过结果和具体阻塞,不得只写「tests passed」。
7. Phase 1A 明确不做的事情
为避免 scope creep,计划文档显式列出了 Phase 1A 不交付的内容:
- Lead agent、native subagent、embedded client 的 Layer 1 工具过滤。
DeferredToolCatalog和tool_search的授权集成。- Layer 2
GuardrailMiddleware自动装配以及与显式 guardrail 的组合顺序。 DeerFlowClient现有 skill filter 缺口修复。- route、model、skill、sandbox、MCP server 的实际授权接线。
- provider 缓存、跨 build singleton 或热更新生命周期优化。
- 前端权限展示。
这些工作进入 Phase 1B 或后续独立 PR;Phase 1A 不提前加入未被消费的执行逻辑。
8. Phase 1B 前置验收清单
进入下一阶段前,以下清单必须全部勾选:
- [ ] Principal 的每个字段都有明确权威来源。
- [ ] Gateway 的服务端字段不可通过两个 config section 伪造。
- [ ] lead/subagent/adapter 使用同一 builder 语义。
- [ ] 未知角色、非法策略和 provider 构造失败均有明确异常。
- [ ] RBAC 的同步、异步、批量过滤结果一致。
- [ ] Phase 1A-1 与 1A-2 的决策已追加到 implementation notes。
- [ ] 分支已 rebase 最新 upstream/main。
- [ ] 未提前修改配置版本号。
9. 从仓库源码复核 Phase 1A 的落点
如果你需要在当前仓库中复核 Phase 1A 各条设计是否落实,可以按以下路径索引快速定位:
| 设计点 | 仓库位置 |
|---|---|
| 唯一 Principal builder(纯函数、严格布尔、attributes 复制) | principal.py |
Gateway 防伪清洗 + is_internal 权威写入 |
services.py |
subagent 身份完整传递(含严格 is_internal) |
task_tool.py |
GuardrailRequest 兼容字段 |
guardrails/provider.py |
| adapter 复用 builder 映射 Principal | adapter.py |
| RBAC 构造期编译 + deny 优先 + 显式资源映射 | rbac.py |
| provider factory 两阶段解析与 Protocol 校验 | runtime.py |
| 行为测试 | test_authorization_principal.py、test_rbac_authorization_provider.py、test_authorization_runtime.py、test_gateway_services.py |
这套「语义锁 + 失败测试先行 + 明确不做清单」的写法,本质上把安全敏感代码的实现自由度压到最低:身份字段的权威来源、异常边界和 fail-closed 语义都在合并前被规则固化,后续 Phase 1B 的 Layer 1/Layer 2 接线只需消费这些已被锁死的契约,而无需重新讨论「身份从哪来、未知角色怎么办」这类根本问题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00