Miracle778 5a699e24a1
feat(guardrails): expose authenticated runtime context in GuardrailRequest (#3665)
* 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>
2026-06-21 16:08:25 +08:00

63 lines
1.7 KiB
Python

"""GuardrailProvider protocol and data structures for pre-tool-call authorization."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
@dataclass
class GuardrailRequest:
"""Context passed to the provider for each tool call."""
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
@dataclass
class GuardrailReason:
"""Structured reason for an allow/deny decision (OAP reason object)."""
code: str
message: str = ""
@dataclass
class GuardrailDecision:
"""Provider's allow/deny verdict (aligned with OAP Decision object)."""
allow: bool
reasons: list[GuardrailReason] = field(default_factory=list)
policy_id: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@runtime_checkable
class GuardrailProvider(Protocol):
"""Contract for pluggable tool-call authorization.
Any class with these methods works - no base class required.
Providers are loaded by class path via resolve_variable(),
the same mechanism DeerFlow uses for models, tools, and sandbox.
"""
name: str
def evaluate(self, request: GuardrailRequest) -> GuardrailDecision:
"""Evaluate whether a tool call should proceed."""
...
async def aevaluate(self, request: GuardrailRequest) -> GuardrailDecision:
"""Async variant."""
...