* docs: guardrail runtime attribution spec * docs: guardrail request attribution implementation plan * feat(guardrails): add runtime user context and attribution fields to GuardrailRequest Extend GuardrailRequest with optional runtime attribution fields so that pluggable GuardrailProviders can access authenticated user context and tool-call-level attribution: - Gateway injects user_role, oauth_provider, oauth_id into runtime context alongside the existing user_id (server-authenticated only, client spoofing prevented) - GuardrailRequest gains: user_id, user_role, oauth_provider, oauth_id, run_id, tool_call_id (all optional, backward compatible) - GuardrailMiddleware reads these from ToolCallRequest.runtime.context - thread_id now actually populated from context (was always None before) - Tests: 15 new/expanded tests covering Gateway injection, runtime context reading, partial/missing fields, and client spoofing prevention - Docs: new Runtime Attribution section in GUARDRAILS.md with provider example and YAML policy illustration * fix(guardrails): propagate attribution to subagents * fix(guardrails): complete subagent attribution propagation --------- Co-authored-by: Miracle778 <miracle778@no-reply.com>
5.8 KiB
GuardrailRequest 运行时用户上下文与归因字段补充
概述
为 GuardrailRequest 补充可选的运行时用户上下文和工具调用归因字段,使可插拔 GuardrailProvider 能访问 DeerFlow 已认证用户、外部身份映射、run/thread/tool-call 定位信息。
本设计不新增治理系统、不定义统一 policy schema,也不改变默认 allow/deny 行为。它只把 DeerFlow 运行时已经掌握的上下文传给 provider。
背景
DeerFlow 已通过 GuardrailMiddleware + 可插拔 GuardrailProvider 实现工具调用前授权。当前 GuardrailDecision 已能表达 allow/deny、原因、policy_id 和 metadata;缺口在 GuardrailRequest 侧:provider 只能看到 tool_name 和 tool_input,无法可靠知道“谁发起了这次工具调用”以及“这次调用属于哪个 run/tool_call”。
源码中 DeerFlow 已有用户身份模型:
users.id:DeerFlow 内部稳定用户 ID。users.system_role:admin/user。users.oauth_provider、users.oauth_id:未来 OAuth/SSO 外部身份映射字段,local user 下可为空。
这些字段当前已存在于认证后的 request.state.user,但 run-time middleware/tool 阶段只能通过 runtime.context 访问上下文。因此应在 Gateway 构建 run config 时注入 server-authenticated user context,再由 GuardrailMiddleware 消费。
改动范围
app/gateway/services.py:inject_authenticated_user_context()注入更多 authenticated user context。guardrails/provider.py:GuardrailRequest新增 optional 字段。guardrails/middleware.py:从ToolCallRequest.runtime.context读取字段。tests/test_setup_agent_e2e_user_isolation.py:验证 Gateway 注入到 runtime context。tests/test_guardrail_middleware.py:验证 runtime context 进入GuardrailRequest。backend/docs/GUARDRAILS.md:更新 custom provider 示例。
设计
GuardrailRequest 字段
@dataclass
class GuardrailRequest:
tool_name: str
tool_input: dict[str, Any]
agent_id: str | None = None
thread_id: str | None = None
is_subagent: bool = False
timestamp: str = ""
user_id: str | None = None
user_role: str | None = None
oauth_provider: str | None = None
oauth_id: str | None = None
run_id: str | None = None
tool_call_id: str | None = None
所有新增字段均为 optional,缺失时保持 None。GuardrailDecision 不变。
字段来源
| 字段 | 来源 | 说明 |
|---|---|---|
user_id |
request.state.user.id → runtime.context["user_id"] |
DeerFlow 内部稳定用户 ID |
user_role |
request.state.user.system_role → runtime.context["user_role"] |
可用于简单 role-based policy |
oauth_provider |
request.state.user.oauth_provider → runtime.context["oauth_provider"] |
OAuth/SSO 外部 provider,local user 可为空 |
oauth_id |
request.state.user.oauth_id → runtime.context["oauth_id"] |
外部 provider subject/user id,local user 可为空 |
run_id |
_build_runtime_context() 写入 runtime.context["run_id"] |
run 级审计归因 |
thread_id |
_build_runtime_context() 写入 runtime.context["thread_id"] |
修正已有字段未填充问题 |
tool_call_id |
request.tool_call.get("id") |
单次 tool call 定位 |
Gateway 注入只信任服务端认证态 request.state.user。客户端 body.context 里的 user_id/user_role/oauth_* 不应覆盖 authenticated user。
收益
稳定审计归因
user_id/run_id/thread_id/tool_call_id 让 provider 或外部审计系统能回答:
- 哪个 DeerFlow 用户触发了 tool call?
- 哪个 run 里发生了 deny?
- 同一轮中多次同名工具调用时,具体是哪一次?
可读的本地策略示例
user_id 是 UUID,不适合直接写人工 policy。user_role 可以支持简单示例:
field: role_tool_key
operator: eq
value: admin:bash
provider 可派生:
role_tool_key = f"{request.user_role or ''}:{request.tool_name}"
外部身份映射
oauth_provider/oauth_id 保留了未来接 OAuth/SSO/IAM 时的外部 subject 信息。当前 OAuth 路由仍是 placeholder,local user 下这些字段通常为 None,但字段 optional,不影响现有部署。
兼容性
- 所有新增字段 optional。
- 现有 provider 不读取新字段时行为不变。
GuardrailDecision不变。- 未认证或无 runtime context 时字段为
None。 - local user 没有 OAuth 信息时
oauth_provider/oauth_id为None。 agent_id语义不变,仍保持现有 passport/agent hint 含义。
测试
新增或扩展以下测试:
| 测试 | 覆盖 |
|---|---|
TestConfigAssembly::test_authenticated_user_context_includes_role_and_oauth_identity |
Gateway 将 user_id/user_role/oauth_provider/oauth_id 注入 runtime context |
TestConfigAssembly::test_client_supplied_user_id_is_overridden |
客户端伪造 identity context 不覆盖服务端认证态 |
TestGuardrailRequestAttribution::test_authenticated_user_context_present |
runtime.context 中用户上下文进入 GuardrailRequest |
TestGuardrailRequestAttribution::test_all_attribution_fields_present |
用户上下文 + run/tool_call 归因字段同时传递 |
| 缺失 runtime/context 测试 | 字段保持 None,向后兼容 |
验证命令:
cd backend
PYTHONPATH=. uv run pytest \
tests/test_guardrail_middleware.py::TestGuardrailRequestAttribution \
tests/test_setup_agent_e2e_user_isolation.py::TestConfigAssembly -v
未涉及
- 不新增 central governance subsystem。
- 不新增 DeerFlow 内置 policy schema。
- 不修改 MCP 配置机制。
- 不修改 OAuth/SSO 实现状态。
- 不让 GuardrailMiddleware 直接依赖 FastAPI request、DB 或 auth repository。