Miracle778 e5d361876a
feat(guardrails): persist security interventions as run events (#3837)
* feat: record guardrail decisions in run events

Persist security-relevant GuardrailMiddleware outcomes (deny and
provider-error fail-open/fail-closed) as middleware:guardrail run
events via RunJournal.record_middleware(), mirroring
SafetyFinishReasonMiddleware. Recording is best-effort: it reads
__run_journal from the runtime context and swallows persistence
failures, so tool execution behavior is unchanged.

The audit payload records tool name/id, agent id, subagent flag, user
role, allow decision, policy id, reason codes/messages, fail_closed
mode, and provider_error flag. Tool input/args and identity fields
(user_id, oauth_*) are deliberately excluded to avoid persisting the
sensitive content being blocked.

The fail-closed provider-error branch returns the denied message
directly from the except block so it records exactly once and does
not fall through to the generic deny branch.

* docs: clarify guardrail journal runtime boundary

* fix: preserve subagent attribution in guardrail events

* test: align guardrail event attribution fixtures

* refactor(guardrails): resolve runtime context once per tool call

Extract `_resolve_context` and thread the already-resolved context dict
through `_build_request` and `_record_guardrail_event` so the
getattr/runtime.context chain runs once per wrap_tool_call instead of twice.

Also document the `is_subagent` field boundary: native subagents do not
inherit __run_journal, so the field is structurally False in persisted
records today; custom runtimes may still supply it with attribution.

Addresses review feedback on #3837 (context-read-twice cleanup and the
is_subagent trade-off note). No behavior change.
2026-07-03 16:08:41 +08:00
..