Summary
Post-execution control check failures in the @control() decorator silently return unchecked output to the caller (fail-open), while pre-execution failures correctly block execution (fail-closed).
Motivation
Agent Control is a safety system. When a post-execution control check fails (e.g. server timeout, network blip, serialization error), the LLM output bypasses all safety controls and reaches the end user. The only signal is an ERROR-level log line — no exception, no indication to the caller. This creates a silent safety gap during transient infrastructure failures.
Current behavior
Pre-execution: if the control check throws a non-ControlViolationError exception, a RuntimeError is raised and the function never executes. This is fail-closed. (control_decorators.py L695-700)
Post-execution: if the control check throws the same kind of exception, it is caught, logged, and silently swallowed. The function output is returned to the caller unchecked. This is fail-open. (control_decorators.py L719-720)
Expected behavior
Post-execution control check failures should also fail-closed — raise a RuntimeError so the caller knows the output was not verified, matching the pre-execution behavior. This prevents unchecked output from silently reaching users during transient failures.
Note: this is a design trade-off. The current behavior may be intentional (the function already executed, so returning the output avoids a wasted call). If fail-open is the desired default, consider at minimum offering a fail_closed=True option on the @control() decorator so users with strict safety requirements can opt in.
Reproduction (if bug)
Start an agent with agent_control.init(...) and a post-execution deny control (e.g. PII regex on output)
Decorate a function with @control()
Cause the control server to be temporarily unreachable (e.g. restart it, or set an invalid server_url)
Call the decorated function — the LLM runs, produces output containing PII
Observe: the output is returned to the caller with no error. Only an ERROR log line appears.
Proposed solution (optional)
Minimal fix: add raise RuntimeError(...) in the post-execution except Exception block to match pre-execution behavior. One-line change + test update.
Alternative: keep fail-open as default but add a fail_closed_post=True parameter to @control() so users can opt into strict mode for safety-critical paths.
Trade-off: fail-closed on post-execution means a server blip causes a RuntimeError even though the function already ran (tokens spent, side effects happened). Fail-open means unsafe output can silently reach users. The question is which failure mode is more acceptable for a safety control system.
**_(I already have fix, just need to confirm if it's a accepted behaviour as stated in line 857 in test_control_decorators
@pytest.mark.asyncio
async def test_other_exceptions_logged_in_post_execution(self, mock_agent, mock_safe_response):
"""Test that non-control exceptions are logged (not raised) in post-execution."""
)_**
Additional context
The existing test test_other_exceptions_logged_in_post_execution explicitly asserts the fail-open behavior with the docstring "Test that non-control exceptions are logged (not raised) in post-execution", suggesting this was a deliberate design choice.
Pre-execution fail-closed: control_decorators.py Line 693-700
Post-execution fail-open: control_decorators.py Line 717-720
Summary
Post-execution control check failures in the @control() decorator silently return unchecked output to the caller (fail-open), while pre-execution failures correctly block execution (fail-closed).
Motivation
Agent Control is a safety system. When a post-execution control check fails (e.g. server timeout, network blip, serialization error), the LLM output bypasses all safety controls and reaches the end user. The only signal is an ERROR-level log line — no exception, no indication to the caller. This creates a silent safety gap during transient infrastructure failures.
Current behavior
Pre-execution: if the control check throws a non-ControlViolationError exception, a RuntimeError is raised and the function never executes. This is fail-closed. (control_decorators.py L695-700)
Post-execution: if the control check throws the same kind of exception, it is caught, logged, and silently swallowed. The function output is returned to the caller unchecked. This is fail-open. (control_decorators.py L719-720)
Expected behavior
Post-execution control check failures should also fail-closed — raise a RuntimeError so the caller knows the output was not verified, matching the pre-execution behavior. This prevents unchecked output from silently reaching users during transient failures.
Note: this is a design trade-off. The current behavior may be intentional (the function already executed, so returning the output avoids a wasted call). If fail-open is the desired default, consider at minimum offering a fail_closed=True option on the @control() decorator so users with strict safety requirements can opt in.
Reproduction (if bug)
Start an agent with agent_control.init(...) and a post-execution deny control (e.g. PII regex on output)
Decorate a function with @control()
Cause the control server to be temporarily unreachable (e.g. restart it, or set an invalid server_url)
Call the decorated function — the LLM runs, produces output containing PII
Observe: the output is returned to the caller with no error. Only an ERROR log line appears.
Proposed solution (optional)
Minimal fix: add raise RuntimeError(...) in the post-execution except Exception block to match pre-execution behavior. One-line change + test update.
Alternative: keep fail-open as default but add a fail_closed_post=True parameter to @control() so users can opt into strict mode for safety-critical paths.
Trade-off: fail-closed on post-execution means a server blip causes a RuntimeError even though the function already ran (tokens spent, side effects happened). Fail-open means unsafe output can silently reach users. The question is which failure mode is more acceptable for a safety control system.
**_(I already have fix, just need to confirm if it's a accepted behaviour as stated in line 857 in test_control_decorators
@pytest.mark.asyncio
async def test_other_exceptions_logged_in_post_execution(self, mock_agent, mock_safe_response):
"""Test that non-control exceptions are logged (not raised) in post-execution."""
)_**
Additional context
The existing test test_other_exceptions_logged_in_post_execution explicitly asserts the fail-open behavior with the docstring "Test that non-control exceptions are logged (not raised) in post-execution", suggesting this was a deliberate design choice.
Pre-execution fail-closed: control_decorators.py Line 693-700
Post-execution fail-open: control_decorators.py Line 717-720