Repository navigation
fix(slack): filter delivered gate routes by raw gate string (auth vs approval) - #4844
Conversation
When a conversation had both a live `gate:auth-*` record and a live
`gate:approval-*` record in the same `DeliveredGateRouteStore`
fingerprint bucket (e.g. a previously-started-but-unfinished OAuth
flow), a bare "approve" triggered `AmbiguousGate` ("Multiple requests
are pending…") instead of resolving the single approval gate. Root
cause: `load_delivered_routes_for_envelope` had no gate-kind filter,
so auth-gate records counted toward the `live.len()` ambiguity check
alongside approval-gate records.
Fix: add an optional `gate_kind_filter: Option<fn(&GateRef) -> bool>`
parameter to `load_delivered_routes_for_envelope` (and threaded through
`select_delivered_gate_route`). The approval path passes
`Some(is_approval_gate_ref)` so only `gate:approval-*` records count;
the auth path passes `Some(is_auth_gate_ref)` so only `gate:auth-*` /
`gate:hook-auth-*` records count. The filter is applied after the
expiry/actor checks and before the ambiguity gate, using the
already-public typed predicates from `approval_interaction` and
`auth_interaction` — no new string literals introduced.
Existing test constants `GATE` and `GATE_B` (previously
`gate:approve-slack{,-b}`) are updated to use real `gate:approval-`
prefixed UUIDs so the approval-path filter exercises the correct code
path in all related delivered-route tests.
Regression test `bare_approve_with_one_approval_and_one_stale_auth_gate_resolves_approval`
drives the full inbound→workflow resolution path (Slack e2e harness,
`ForeignScopeApprovalService`, two records in the same DM fingerprint
bucket) and asserts exactly one approval resolve request is forwarded.
This test would fail on the pre-fix path (live.len()==2 → Ambiguous).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…approval)
FIX 1 — change gate_kind_filter predicate from fn(&GateRef)->bool to fn(&str)->bool
The old signature forced each candidate route's stored gate string to be
re-wrapped into a GateRef before the predicate ran. This had two bugs:
• Per-route allocation: every route required a GateRef::new() call just to
run a prefix check.
• Silent drop: any route whose stored string failed GateRef::new() validation
was dropped before the predicate ran, meaning the filter never executed on
those routes.
The new fn(&str)->bool predicate receives the raw stored gate string directly,
eliminating both issues. Updated:
- is_approval_gate_ref (approval_interaction/gate_ref.rs)
- is_auth_gate_ref (auth_interaction/gate_ref.rs)
- load_delivered_routes_for_envelope and select_delivered_gate_route
signatures in workflow.rs
- All callers in reborn_services.rs, slack_delivery.rs, turn_events.rs
updated to pass gate_ref.as_str() instead of &gate_ref.
FIX 2 — add symmetric bare-auth-deny e2e test covering the regression
bare_auth_deny_with_stale_approval_route_selects_auth_route_not_approval:
A bare auth-deny arrives in a conversation that has a stale APPROVAL gate
route in the same conversation fingerprint bucket. Asserts the auth-kind
filter drops the approval route (Miss), and the stale approval route's
run_id is never forwarded to the auth service as a run_id_hint. Mirrors
scoped_approval_two_live_routes_same_conversation_rejects_ambiguous in the
opposite direction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughGate reference predicates ( ChangesGate-kind filtering for delivered route resolution
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Code Review
This pull request refactors the gate reference checking functions is_approval_gate_ref and is_auth_gate_ref to accept &str instead of &GateRef. It introduces a gate_kind_filter parameter to route loading and selection functions in the workflow module. This filter ensures that when resolving delivered routes, approval routes are separated from auth routes, preventing lingering gates of a different kind in the same conversation bucket from causing spurious ambiguity errors. Comprehensive unit and integration tests have been added to verify this behavior. There are no review comments, and I have no additional feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e85083f25e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
henrypark133
left a comment
There was a problem hiding this comment.
Code Review (multi-agent)
Intent: Fix Slack delivered-route filtering so auth and approval gates are matched by raw gate string, with symmetric regression coverage.
Stats: 5 findings (from 5 raw, 5 after dedup) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0
Tests
- Medium Untested invalid stored approval gate route (
crates/ironclaw_product_workflow/src/workflow.rs:659-666, confidence 75) — anchor:crates/ironclaw_product_workflow/src/workflow.rs:659
The new raw-string gate-kind filter can now select a stored route whose string starts withgate:approval-but still failsGateRefvalidation, causing the delivered approval fallback to returnInvalidGateRef. - Medium Untested invalid stored auth gate route (
crates/ironclaw_product_workflow/src/workflow.rs:736-743, confidence 75) — anchor:crates/ironclaw_product_workflow/src/workflow.rs:736
The auth delivered-route fallback now filters raw stored strings before reconstructingGateRef, so malformed auth-prefixed stored routes can now exercise theInvalidGateRefpath.
Conventions
- Medium Raw invalid stored gate refs are still untested (
crates/ironclaw_product_workflow/src/workflow.rs:473-476, confidence 75) — anchor:.claude/rules/testing.md:24
The added tests cover cross-kind contamination with valid stored refs, but not the stated invalid-stored-string regression.
Local Patterns
- Low Test comment claims coverage the fixture does not exercise (
crates/ironclaw_product_workflow/tests/product_workflow_contract.rs:2062-2067, confidence 75) — anchor:crates/ironclaw_product_workflow/tests/product_workflow_contract.rs:2062
The fixture uses a valid approval gate while the comment says it covers invalid stored strings being silently dropped before the predicate ran.
Maintainability
- Low Gate-kind filter does not need a None mode (
crates/ironclaw_product_workflow/src/workflow.rs:477-477, confidence 75) — anchor:crates/ironclaw_product_workflow/src/workflow.rs:477
The selector is private and both approval/auth callers always pass a predicate, so the optional unfiltered branch appears unused.
The delivered-route gate-kind filter ran before the expected_gate_ref exact-match check, so an explicitly-named generic/legacy gate (whose stored string is not a typed approval/auth prefix) was dropped before the exact match could forward it, falling through to BindingRequired/MissingAuth. For an exact-ref lookup the kind filter can only total-drop, never disambiguate; it is only meaningful for bare lookups. Apply gate_kind_filter only when expected_gate_ref is None. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (3)
crates/ironclaw_product_workflow/src/workflow.rs (3)
481-481: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winOptional gate-kind filter has no None-mode caller.
Both
resolve_via_delivered_approval_route(line 653) andresolve_via_delivered_auth_route(line 730) always passSome(...). TheNonebranch at lines 546-553 is unreachable. Making the parameter mandatory (fn(&str) -> bool) eliminates the dead branch and clarifies that delivered-route fallback is always typed by interaction kind.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_product_workflow/src/workflow.rs` at line 481, The gate_kind_filter parameter is defined as Optional but all callers always provide a value, making the None-handling branch at lines 546-553 unreachable dead code. Change the gate_kind_filter parameter from Option<fn(&str) -> bool> to fn(&str) -> bool (removing the Option wrapper) to make it mandatory, then remove the unreachable None branch in the code that checks for None. The two callers at resolve_via_delivered_approval_route and resolve_via_delivered_auth_route already pass Some(...) so they will not need changes.
742-746:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUntested InvalidGateRef path for malformed stored auth route.
The auth delivered-route fallback filters raw stored strings before reconstructing
GateRef, so a stored string like an overlonggate:auth-*route can passis_auth_gate_refand then hit theInvalidGateReferror path. Existing auth coverage uses valid stale approval routes and never exercises this newly-reachable malformed stored-route path..claude/rules/testing.mdrequires regression tests for bug fixes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_product_workflow/src/workflow.rs` around lines 742 - 746, The InvalidGateRef error path in the GateRef::new() call at line 742-746 is untested because existing auth coverage only uses valid stale approval routes. Add a regression test that exercises this path by testing with a malformed stored auth route (such as an overlong gate:auth-* route) that passes the is_auth_gate_ref validation filter but then fails when attempting to construct the GateRef object. This test should verify that the AuthInteractionRejected error with InvalidGateRef kind is properly returned for such malformed stored routes.
665-669:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUntested InvalidGateRef path for malformed stored approval route.
The raw-string filter can now select a stored route whose
gate_refstring passesis_approval_gate_refbut failsGateRef::new()validation (e.g., overlonggate:approval-*exceeding validation limits). The delivered approval fallback would returnInvalidGateRef, but no test seeds a malformed stored approval route exercising this error branch..claude/rules/testing.mdrequires regression coverage for bug fixes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_product_workflow/src/workflow.rs` around lines 665 - 669, The InvalidGateRef error path in the GateRef::new() call is untested when handling a malformed stored approval route. Add a regression test case that seeds a stored approval route with a gate_ref string that passes is_approval_gate_ref validation but exceeds GateRef::new() validation limits (e.g., an overlong gate:approval-* string). The test should verify that when the approval interaction path is exercised with this malformed route, it returns a ProductWorkflowError::ApprovalInteractionRejected error with ApprovalInteractionRejectionKind::InvalidGateRef, ensuring the error handling in the GateRef::new() map_err block is properly covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@crates/ironclaw_product_workflow/src/workflow.rs`:
- Line 481: The gate_kind_filter parameter is defined as Optional but all
callers always provide a value, making the None-handling branch at lines 546-553
unreachable dead code. Change the gate_kind_filter parameter from
Option<fn(&str) -> bool> to fn(&str) -> bool (removing the Option wrapper) to
make it mandatory, then remove the unreachable None branch in the code that
checks for None. The two callers at resolve_via_delivered_approval_route and
resolve_via_delivered_auth_route already pass Some(...) so they will not need
changes.
- Around line 742-746: The InvalidGateRef error path in the GateRef::new() call
at line 742-746 is untested because existing auth coverage only uses valid stale
approval routes. Add a regression test that exercises this path by testing with
a malformed stored auth route (such as an overlong gate:auth-* route) that
passes the is_auth_gate_ref validation filter but then fails when attempting to
construct the GateRef object. This test should verify that the
AuthInteractionRejected error with InvalidGateRef kind is properly returned for
such malformed stored routes.
- Around line 665-669: The InvalidGateRef error path in the GateRef::new() call
is untested when handling a malformed stored approval route. Add a regression
test case that seeds a stored approval route with a gate_ref string that passes
is_approval_gate_ref validation but exceeds GateRef::new() validation limits
(e.g., an overlong gate:approval-* string). The test should verify that when the
approval interaction path is exercised with this malformed route, it returns a
ProductWorkflowError::ApprovalInteractionRejected error with
ApprovalInteractionRejectionKind::InvalidGateRef, ensuring the error handling in
the GateRef::new() map_err block is properly covered.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 85bf8463-46e9-427a-9204-eb59158130a6
📒 Files selected for processing (2)
crates/ironclaw_product_workflow/src/workflow.rscrates/ironclaw_product_workflow/tests/product_workflow_contract.rs
…d stored gate refs - gate_kind_filter is now a required fn(&str)->bool (both callers always supplied one); removes the unreachable None branch. - Add caller-level tests for stored gate strings that pass the kind prefix predicate but fail GateRef::new, exercising the InvalidGateRef branch that the raw-string filter newly makes reachable. - Correct a test comment that overclaimed invalid-stored-route coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…approval) (nearai#4844) * fix(slack): exclude auth gates from bare-approve ambiguity check When a conversation had both a live `gate:auth-*` record and a live `gate:approval-*` record in the same `DeliveredGateRouteStore` fingerprint bucket (e.g. a previously-started-but-unfinished OAuth flow), a bare "approve" triggered `AmbiguousGate` ("Multiple requests are pending…") instead of resolving the single approval gate. Root cause: `load_delivered_routes_for_envelope` had no gate-kind filter, so auth-gate records counted toward the `live.len()` ambiguity check alongside approval-gate records. Fix: add an optional `gate_kind_filter: Option<fn(&GateRef) -> bool>` parameter to `load_delivered_routes_for_envelope` (and threaded through `select_delivered_gate_route`). The approval path passes `Some(is_approval_gate_ref)` so only `gate:approval-*` records count; the auth path passes `Some(is_auth_gate_ref)` so only `gate:auth-*` / `gate:hook-auth-*` records count. The filter is applied after the expiry/actor checks and before the ambiguity gate, using the already-public typed predicates from `approval_interaction` and `auth_interaction` — no new string literals introduced. Existing test constants `GATE` and `GATE_B` (previously `gate:approve-slack{,-b}`) are updated to use real `gate:approval-` prefixed UUIDs so the approval-path filter exercises the correct code path in all related delivered-route tests. Regression test `bare_approve_with_one_approval_and_one_stale_auth_gate_resolves_approval` drives the full inbound→workflow resolution path (Slack e2e harness, `ForeignScopeApprovalService`, two records in the same DM fingerprint bucket) and asserts exactly one approval resolve request is forwarded. This test would fail on the pre-fix path (live.len()==2 → Ambiguous). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(slack): filter delivered gate routes by raw gate string (auth vs approval) FIX 1 — change gate_kind_filter predicate from fn(&GateRef)->bool to fn(&str)->bool The old signature forced each candidate route's stored gate string to be re-wrapped into a GateRef before the predicate ran. This had two bugs: • Per-route allocation: every route required a GateRef::new() call just to run a prefix check. • Silent drop: any route whose stored string failed GateRef::new() validation was dropped before the predicate ran, meaning the filter never executed on those routes. The new fn(&str)->bool predicate receives the raw stored gate string directly, eliminating both issues. Updated: - is_approval_gate_ref (approval_interaction/gate_ref.rs) - is_auth_gate_ref (auth_interaction/gate_ref.rs) - load_delivered_routes_for_envelope and select_delivered_gate_route signatures in workflow.rs - All callers in reborn_services.rs, slack_delivery.rs, turn_events.rs updated to pass gate_ref.as_str() instead of &gate_ref. FIX 2 — add symmetric bare-auth-deny e2e test covering the regression bare_auth_deny_with_stale_approval_route_selects_auth_route_not_approval: A bare auth-deny arrives in a conversation that has a stale APPROVAL gate route in the same conversation fingerprint bucket. Asserts the auth-kind filter drops the approval route (Miss), and the stale approval route's run_id is never forwarded to the auth service as a run_id_hint. Mirrors scoped_approval_two_live_routes_same_conversation_rejects_ambiguous in the opposite direction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slack): skip gate-kind filter for exact-named gate refs The delivered-route gate-kind filter ran before the expected_gate_ref exact-match check, so an explicitly-named generic/legacy gate (whose stored string is not a typed approval/auth prefix) was dropped before the exact match could forward it, falling through to BindingRequired/MissingAuth. For an exact-ref lookup the kind filter can only total-drop, never disambiguate; it is only meaningful for bare lookups. Apply gate_kind_filter only when expected_gate_ref is None. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slack): make delivered-route kind filter mandatory + cover invalid stored gate refs - gate_kind_filter is now a required fn(&str)->bool (both callers always supplied one); removes the unreachable None branch. - Add caller-level tests for stored gate strings that pass the kind prefix predicate but fail GateRef::new, exercising the InvalidGateRef branch that the raw-string filter newly makes reachable. - Correct a test comment that overclaimed invalid-stored-route coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
FIX 1 — replace
fn(&GateRef)->boolwithfn(&str)->boolforgate_kind_filterThe
load_delivered_routes_for_envelopeandselect_delivered_gate_routefunctions previously tookgate_kind_filter: Option<fn(&GateRef) -> bool>. This had two bugs:GateRefviaGateRef::new(r.gate_ref.clone())just to run a prefix check.GateRef::new()validation was silently discarded before the predicate ran, meaning the filter never executed on such routes (they were invisible to it).Changed to
Option<fn(&str) -> bool>so the predicate receives the raw stored string directly. TheGateRef::newwrap in the filter body is gone.Updated predicates:
is_approval_gate_ref(approval_interaction/gate_ref.rs) — now takes&stris_auth_gate_ref(auth_interaction/gate_ref.rs) — now takes&strUpdated callers that passed
&GateRef:reborn_services.rsfrom_gate_shape→ passes.as_str()slack_delivery.rs(two sites) → passes.as_str()projection/turn_events.rs→ passes.as_str()FIX 2 — symmetric bare-auth-deny integration test
New test:
bare_auth_deny_with_stale_approval_route_selects_auth_route_not_approvalScenario: a stale APPROVAL gate route is stored in the delivered-route store under the same conversation fingerprint as an incoming bare auth-deny. Asserts:
is_auth_gate_ref) drops the approval route → Missrun_idis never forwarded to the auth service as arun_id_hintgate_ref, not the stale approval oneThis is the symmetric counterpart of
scoped_approval_two_live_routes_same_conversation_rejects_ambiguous(which verifies the opposite direction). Under the oldfn(&GateRef)shape, a stale approval route with an invalid stored string would have been silently dropped before the predicate ran — leaving the auth filter's correctness unverifiable for those routes.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests