Skip to content

Commit 92cd1b7

Browse files
committed
feat: ADR 0032 M3 Slice B — enforce plan gate via interceptor, remove inline gate
Second of the two §13.3 landings. Flips the plan-gate interceptor to enforce mode (raise_on_deny=True) and removes the now-redundant inline evaluate_write_gate call from AgentRunner. The plan gate is now solely the EventSpine interceptor on TOOL_CALL_REQUESTED. Safe because Slice A (26446d5) proved, with the inline gate still authoritative, that the interceptor's decision is identical per reason code (parity test green on a full acceptance run). Denials, reason codes, and pending/failure status are unchanged. Constraint: behavior-preserving; same denials and reason codes as the inline gate Slice A proved equivalent; no test assertion changed Tested: lifecycle, adversarial + first-hour + from-plan 36, smoke 200, acceptance 646/646, full mypy clean 1009 files, ruff check+format clean Confidence: high Roadmap-Status: unchanged
1 parent 26446d5 commit 92cd1b7

1 file changed

Lines changed: 11 additions & 20 deletions

File tree

teaagent/runner/_core.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,18 @@ def __init__(
172172
skip_plan_check=skip_plan_check,
173173
)
174174

175-
# M3-T002 Slice A: register the plan gate interceptor in SHADOW mode.
176-
# It computes its decision on TOOL_CALL_REQUESTED but does NOT veto;
177-
# the inline evaluate_write_gate below stays authoritative. The parity
178-
# test asserts interceptor decision == inline decision per reason code.
179-
# Slice B (separate commit) flips this to enforce and removes the inline
180-
# gate.
175+
# M3-T002 Slice B: the plan gate is now the authoritative EventSpine
176+
# interceptor (enforce mode). It vetoes TOOL_CALL_REQUESTED by raising
177+
# ToolPermissionError when the write is blocked by plan policy or
178+
# read-only lint. Slice A proved its decision equals the (now removed)
179+
# inline gate per reason code.
181180
self._plan_gate_interceptor = PlanGateInterceptor(
182181
self.plan_validator,
183-
raise_on_deny=False,
182+
raise_on_deny=True,
184183
)
185184
self.event_spine.register_interceptor(
186185
self._plan_gate_interceptor,
187-
name='plan_gate_shadow',
186+
name='plan_gate',
188187
)
189188

190189
self.auto_mode_manager = AutoModeManager(
@@ -641,9 +640,10 @@ def _execute_tool_decision( # noqa: C901
641640
arguments=decision.arguments,
642641
)
643642

644-
# M3-T002 Slice A: emit TOOL_CALL_REQUESTED so the SHADOW plan-gate
645-
# interceptor records its decision (it does not veto). The inline gate
646-
# below remains authoritative until Slice B.
643+
# M3-T002 Slice B: the plan gate is the EventSpine interceptor. Emitting
644+
# TOOL_CALL_REQUESTED runs it; it raises ToolPermissionError if the
645+
# write is blocked. The inline evaluate_write_gate was removed here —
646+
# Slice A proved the interceptor's decision is identical.
647647
tcr_payload: dict[str, Any] = {
648648
'tool_name': decision.tool_name,
649649
}
@@ -658,15 +658,6 @@ def _execute_tool_decision( # noqa: C901
658658
tcr_payload,
659659
)
660660

661-
# M3-T002 Slice A: inline plan gate stays authoritative (removed in B).
662-
gate_error = self.plan_validator.evaluate_write_gate(
663-
tool_name=decision.tool_name,
664-
context=cast(dict[str, Any], context),
665-
tool_arguments=decision.arguments,
666-
)
667-
if gate_error:
668-
raise ToolPermissionError(gate_error)
669-
670661
# Auto mode: block disallowed tools, auto-approve allowed ones
671662
self.auto_mode_manager.validate_tool_allowed(decision.tool_name)
672663
auto_approve_policy = self.auto_mode_manager.get_auto_approve_policy()

0 commit comments

Comments
 (0)