|
| 1 | +# M4 Approval Slice B Blocked — Interceptor Misses jit_state/handler |
| 2 | + |
| 3 | +> **Status:** Blocked by a verified correctness gap, 2026-06-13. Approval |
| 4 | +> Slice A is committed (`2da5d6c`) and safe (interceptor + unit parity, not |
| 5 | +> wired). Slice B (enforce cutover) must NOT land until the gap below is closed. |
| 6 | +
|
| 7 | +## The gap |
| 8 | + |
| 9 | +The inline approval check in `AgentRunner._execute_tool_decision` |
| 10 | +(`teaagent/runner/_core.py`, the `self.approval_policy.assert_allowed(...)` call) |
| 11 | +passes two runtime inputs that the salvaged `ApprovalGateInterceptor` |
| 12 | +(`teaagent/runner/_approval_manager.py`) does **not**: |
| 13 | + |
| 14 | +```python |
| 15 | +# inline (authoritative today): |
| 16 | +self.approval_policy.assert_allowed( |
| 17 | + ..., jit_state=self.approval_manager.jit_state, handler=tool.handler, ... |
| 18 | +) |
| 19 | +# interceptor (would replace it in Slice B): |
| 20 | +self._approval_policy.assert_allowed( |
| 21 | + ..., # no jit_state, no handler |
| 22 | +) |
| 23 | +``` |
| 24 | + |
| 25 | +`ApprovalPolicy.assert_allowed` (`teaagent/policy.py`) uses `jit_state` to merge |
| 26 | +the session's JIT `approved_call_ids` / `session_approved_tools` into the |
| 27 | +decision (verified lines ~23-27 of the method). Without it, a call that the |
| 28 | +session has JIT-approved would be **denied** by the interceptor while the inline |
| 29 | +path **allows** it — a behavior regression in enforce mode. `handler` is |
| 30 | +similarly forwarded and may affect handler-gated decisions. |
| 31 | + |
| 32 | +## Why the unit parity test didn't catch it |
| 33 | + |
| 34 | +`test_approval_gate_interceptor_parity` constructs **fresh** `ApprovalPolicy` |
| 35 | +objects per scenario with no JIT/session state, so interceptor and inline agree |
| 36 | +trivially. The divergence only appears with live `jit_state` — exactly the |
| 37 | +state the interceptor cannot see. The parallel tool's collapsed M4 batch had the |
| 38 | +same incomplete interceptor; its acceptance pass did not exercise a |
| 39 | +JIT-approved-then-enforced path, so the latent regression went unobserved. |
| 40 | + |
| 41 | +## Fix design (for Slice B, before enforce) |
| 42 | + |
| 43 | +1. Give the interceptor access to JIT state — either hold a reference to the |
| 44 | + `RunnerApprovalCoordinator`/`jit_state`, or carry `jit_state` + `handler` |
| 45 | + in the `TOOL_CALL_REQUESTED` payload (payload route keeps the interceptor |
| 46 | + pure but means putting a callable `handler` in an event payload, which is |
| 47 | + ugly and pollutes the audit stream — prefer the reference route). |
| 48 | +2. Extend the **parity test** to cover a JIT-approved call and a |
| 49 | + session-approved tool, asserting interceptor == inline **with** live |
| 50 | + jit_state. This is the assertion that was missing. |
| 51 | +3. Only then: register enforce + remove the inline `assert_allowed` |
| 52 | + (Slice B), with the JIT parity test green. |
| 53 | + |
| 54 | +## Current state |
| 55 | + |
| 56 | +- Committed and safe: `5b5f007` (PlanGateError), `2da5d6c` (approval Slice A: |
| 57 | + interceptor + unit parity, **not wired** — zero behavior change). |
| 58 | +- NOT done: approval Slice B (this blocker), budget Slice A, budget Slice B. |
| 59 | +- The parallel tool's full collapsed M4 remains in `git stash@{0}` |
| 60 | + ("parallel-tool-M4-collapsed-batch-salvage-2026-06-13") for salvage — |
| 61 | + note it shares this jit_state/handler gap. |
| 62 | +- Budget gate has its own trap (see §15 family): the budget interceptor can |
| 63 | + emit `budget_warning` audit events, so a shadow budget interceptor must NOT |
| 64 | + be given `audit=` while inline budget warnings remain, or warnings |
| 65 | + double-write. |
0 commit comments