Repository navigation
refactor(outbound): judge the request edge in the orchestrator - #569
Merged
Merged
Conversation
Moves judge_outbound + OutboundVerdict out of gateway/outbound.py onto the request edge in orchestrator.py, so defend() on the outbound (/proxy/llm and egress) request path is reached from the orchestration layer rather than the gateway. gateway/outbound.py keeps the pure request-reading helpers (reveal, judge_input, body_text, detect_credentials); the defend() call and the refuse decision ride with the orchestrator next to judge_response / judge_egress_response. OutboundVerdict gains a `refused` field decided in judge_outbound, replacing the hand-rolled OutboundVerdict.blocks() = `kinds or flagged or unjudged`. The layer part now goes through refusal_reason() -- the one rule dispose() delegates to -- rather than a second copy of `flag or gap`; the credential dimension is OR'd on top, gated to block. Byte-identical: DefenseVerdict.flagged is defined as `flagged_by is not None`, so refusal_reason(flagged_by, gaps) is non-None in exactly the cases the old blocks() refused. judge_outbound keeps its function-local `from .defense import defend` so the REQUEST edge is patched at trentina.defense.defend, distinct from the RESPONSE edge's trentina.orchestrator.defend (the seam test_egress_http relies on), and its lazy gaps_of so the request edge resolves gaps in the modes namespace, independent of the response edge's dispose()/orchestrator namespaces. One fewer defend() caller in gateway/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K4tUpQP4psJxsQWixULwQj
fatherlinux
enabled auto-merge
October 27, 2026 14:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Moves
judge_outbound+OutboundVerdictout ofgateway/outbound.pyonto the request edge inorchestrator.py. The outbound (/proxy/llmand terminating egress) request path'sdefend()call and refuse decision now live in the orchestration layer, besidejudge_response/judge_egress_response.gateway/outbound.pykeeps the pure request-reading helpers (reveal,judge_input,body_text,detect_credentials).One fewer
defend()caller ingateway/— the Phase 5 "defend()→ one caller (the orchestrator)" target.The drift this deletes
OutboundVerdict.blocks()was a hand-rolledkinds or flagged or unjudged— a second copy of the layer refusal rule. It's replaced by arefusedfield decided injudge_outbound, whose layer part goes throughrefusal_reason()— the one ruledispose()itself delegates to. The credential dimension (outbound-only, outsidedispose()'s model) is OR'd on top, gated toblock.Byte-identical
DefenseVerdict.flaggedis defined asflagged_by is not None(defense.py:148), sorefusal_reason(flagged_by, gaps)is non-None in exactly the cases the oldblocks()refused. The exception/deadline path is unchanged (no verdict ⇒ unjudged ⇒blockrefuses,flagwarns).Test seam
judge_outboundkeeps its function-localfrom .defense import defend, so the REQUEST edge is still patched attrentina.defense.defend, distinct from the RESPONSE edge'strentina.orchestrator.defend(the splittest_egress_httprelies on). It keeps a lazygaps_ofso the request edge resolves gaps in themodesnamespace — deliberately not viadispose(), which would couple its gap derivation to the response edge'score.disposenamespace in a combined crossing. No test gaps-patch changes were needed.Verification
ruff/mypy/ semgrep (0) / Gourmand (0) / Gatehouse (staged diff) all green.test_llm_proxy,test_egress_http,test_orchestrator,test_disposition_parity,test_defense_contract,test_gatekeeper,test_log_hygiene(248 passed).mainshows the same 1-failure pattern with a different OAuth/secret victim, and both victims pass in isolation.🤖 Generated with Claude Code
https://claude.ai/code/session_01K4tUpQP4psJxsQWixULwQj