Skip to content

fix(reborn): cancel stale AuthFlow on Slack auth auto-deny (#4952) - #5014

Merged
henrypark133 merged 6 commits into
mainfrom
fix/slack-auth-stale-flow-4952
Jun 17, 2026
Merged

henrypark133 merged 6 commits into
mainfrom
fix/slack-auth-stale-flow-4952

Conversation

@henrypark133

Copy link
Copy Markdown
Collaborator

Closes #4952.

Problem

When a Slack run blocked on interactive auth is auto-denied — a non-OAuth challenge the Slack surface can't satisfy, or an OAuth URL suppressed by the #4953 send-time DM backstop — the delivery path cancels the run directly via TurnCoordinator::cancel_run but skips AuthFlowManager::cancel_flow. Unlike the canonical AuthInteractionService deny path (which cancels the flow first), this leaves the durable AuthFlow record non-terminal (Pending/AwaitingUser) until it expires.

Severity: low/cleanliness — no security exposure. The run itself terminates correctly. The stale flow is flow-ID-keyed, so it does not block a new auth flow and does not pollute the WebUI pending list (the run is no longer BlockedAuth). The harm is a dangling storage record plus a confusing error if an out-of-order OAuth callback lands on the dead flow_id.

Fix

A narrow BlockedAuthFlowCanceller port (implemented by RebornProductAuthServices over its existing flow_record_source + flow_manager) threaded through the shared cancel_auth_blocked_run helper, so the flow is cancelled alongside the run at all three call sites:

  • live observer non-OAuth deny arm,
  • triggered non-OAuth BlockedAuth arm,
  • OAuth send-time backstop (OAuthTargetNotDm).

Design notes:

  • Fail-graceful on race: the lookup uses include_terminal: false, so an already-terminal/Completed flow (the OAuth-callback-just-landed race) resolves to None and is a no-op.
  • Best-effort: a flow-cancel failure is debug!-logged and never blocks the run cancellation (the user-visible terminal action).
  • Backward-compatible: the handle is Option<Arc<dyn …>> and is None when no flow_record_source is wired (Slack without product-auth) — the run is still cancelled.
  • No boundary changes: reborn composition already owns AuthFlowManager wiring; not a gateway handler, so the ToolDispatcher mandate doesn't apply.

Tests

  • blocked_auth_cancels_stale_auth_flow — live observer caller (test-through-the-caller; asserts cancelled run_id + gate).
  • triggered_non_oauth_auth_cancels_stale_auth_flow — triggered non-OAuth arm.
  • triggered_oauth_backstop_cancels_stale_auth_flow — OAuth send-time backstop arm.
  • cancel_blocked_auth_flow_{cancels_non_terminal_flow,is_noop_when_flow_absent,is_noop_without_flow_record_source} — facade unit tests.

fmt + clippy clean (--features slack-v2-host-beta).

Review

Local 4-reviewer pass (bugs/security/maintainability/tests). Security: no findings. Bugs/maintainability flagged an empty-string gate_ref sentinel in the backstop arm — fixed by making gate_ref an explicit Option<&str> that skips the flow-cancel when absent. The bool→visibility-enum generalization remains deferred (tracked separately) and is out of scope here.

🤖 Generated with Claude Code

henrypark133 and others added 2 commits June 16, 2026 22:07
When a Slack run blocked on interactive auth is auto-denied (non-OAuth
challenge the Slack surface can't satisfy, or an OAuth URL suppressed by
the send-time DM backstop), the delivery path cancelled the run directly
via TurnCoordinator but skipped AuthFlowManager::cancel_flow — unlike the
canonical AuthInteractionService deny path. The AuthFlow record lingered
non-terminal (Pending/AwaitingUser) until expiry. State-drift / cleanliness
gap, no security exposure.

Fix: add a narrow BlockedAuthFlowCanceller port (impl by
RebornProductAuthServices over its flow_record_source + flow_manager) and
thread it through the shared cancel_auth_blocked_run helper so the flow is
cancelled alongside the run at all three sites (live observer, triggered
non-OAuth arm, OAuth send-time backstop).

- include_terminal:false → already-terminal/Completed flows resolve to None
  and are a graceful no-op (handles the OAuth-callback race).
- Best-effort: a flow-cancel failure is debug-logged and does not block the
  run cancellation (the user-visible terminal action).
- None when no flow_record_source is wired (Slack without product-auth) —
  skips flow cancel, still cancels the run. Backward-compatible.
- Regression test drives the live observer caller with a recording fake
  canceller (test-through-the-caller).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address local review (bugs/security/maintainability/tests):
- Make cancel_auth_blocked_run's gate_ref an explicit Option<&str> and skip
  the flow cancel when absent, instead of passing an empty-string sentinel
  that silently no-ops inside the facade (was unwrap_or_default()).
- Add regression coverage for the two previously-untested call sites and the
  facade impl:
  * triggered_non_oauth_auth_cancels_stale_auth_flow (triggered non-OAuth arm)
  * triggered_oauth_backstop_cancels_stale_auth_flow (OAuth send-time backstop)
  * cancel_blocked_auth_flow_{cancels_non_terminal_flow,is_noop_when_flow_absent,
    is_noop_without_flow_record_source} (facade unit tests)
- Assert the cancelled run_id in blocked_auth_cancels_stale_auth_flow.

Security review: no findings (owner+gate scoped lookup, fail-closed terminal
guard, best-effort cancel does not block run cancellation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5014 June 17, 2026 05:33 Destroyed
@railway-app

railway-app Bot commented Jun 17, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5014 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jun 17, 2026 at 7:01 pm

@github-actions github-actions Bot added size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jun 17, 2026
@coderabbitai

coderabbitai Bot commented Jun 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 62ebdd22-dfde-465d-8fc6-75dc1082656c

📥 Commits

Reviewing files that changed from the base of the PR and between a187119 and 66a6830.

📒 Files selected for processing (1)
  • crates/ironclaw_reborn_composition/src/auth.rs

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a blocked-auth flow cancellation capability (Slack v2 host beta) to cancel the durable auth-flow behind a blocked-auth gate, with graceful no-ops when missing, not found, or already terminal.
  • Bug Fixes

    • Improved blocked-auth/OAuth interruption handling by cancelling the run first, then performing best-effort blocked flow cancellation using the configured gate reference; canceller errors are non-fatal.
  • Tests

    • Expanded async coverage for seeded cancellations, absent/missing canceller behavior, invalid gate reference handling, terminal-race normalization, backend error propagation, and Slack delivery/E2E wiring updates.

Walkthrough

Introduces a BlockedAuthFlowCanceller trait and implements it on RebornProductAuthServices by querying and cancelling non-terminal AuthFlow records. The implementation is exposed through RebornRuntime, added as a field on SlackFinalReplyDeliveryServices, and invoked best-effort after successful cancel_run on all Slack blocked-auth auto-deny paths (live observer, triggered non-OAuth, OAuth DM backstop) to clean up stale flow records alongside run cancellation.

Changes

BlockedAuthFlowCanceller trait, implementation, and Slack delivery wiring

Layer / File(s) Summary
BlockedAuthFlowCanceller trait definition and crate re-export
crates/ironclaw_reborn_composition/src/auth_prompt.rs, crates/ironclaw_reborn_composition/src/lib.rs
Defines the BlockedAuthFlowCanceller async trait with documented no-op semantics for missing/terminal flows and re-exports it from the crate root.
RebornProductAuthServices implementation and accessor
crates/ironclaw_reborn_composition/src/auth.rs
Adds as_blocked_auth_flow_canceller conditional accessor and the cancel_blocked_auth_flow implementation that validates gate_ref, queries by (scope, owner_user_id, run_id, gate_ref, include_terminal: false), and cancels via flow_manager.cancel_flow. No-ops for unconfigured projection or absent flow; terminal-race errors (Canceled, FlowAlreadyTerminal) normalize to Ok(()). Comprehensive async Tokio unit tests for successful cancellation, absent flow, race normalization, unconfigured source, error propagation, and invalid gate-ref rejection.
RebornRuntime feature-gated accessor
crates/ironclaw_reborn_composition/src/runtime.rs
Adds blocked_auth_flow_canceller() under #[cfg(feature = "slack-v2-host-beta")], delegating to product_auth.as_blocked_auth_flow_canceller().
SlackFinalReplyDeliveryServices field, cancel_auth_blocked_run integration, and all denial call sites
crates/ironclaw_reborn_composition/src/slack_delivery.rs
Adds auth_flow_canceller field and imports. Extends cancel_auth_blocked_run to accept optional canceller and gate_ref, resolves target owner from scope.explicit_owner_user_id() (fallback to acting user), and performs best-effort cancellation after idempotent cancel_run succeeds. Canceller errors debug-logged and swallowed. Updates wrapper and all three denial paths (live observer non-OAuth, triggered non-OAuth, OAuth OAuthTargetNotDm backstop) to pass gate_ref. Wires canceller into TriggeredRunDeliveryDriver spawned tasks.
Production wiring in slack_host_beta
crates/ironclaw_reborn_composition/src/slack_host_beta.rs
Wires auth_flow_canceller: runtime.blocked_auth_flow_canceller() into both the triggered-run delivery hook and Slack events route mount service bundles.
Test infrastructure and new behavioral tests
crates/ironclaw_reborn_composition/src/slack_delivery.rs
Adds make_services_with_canceller and make_observer_with_canceller helpers for optional injection. Introduces RecordingBlockedAuthFlowCanceller for assertion support. Adds three new async tests covering observer non-OAuth auto-deny, triggered non-OAuth denial, and OAuth DM backstop—each asserting the auth-flow is cancelled exactly once with correct owner/scope. Updates all existing service literals with auth_flow_canceller: None.
E2E harness null-wiring
crates/ironclaw_reborn_composition/src/slack_serve/e2e_tests.rs
Sets auth_flow_canceller: None in three E2E harness builders to satisfy the new struct field.

Sequence Diagram(s)

sequenceDiagram
  participant SlackInbound as Slack Inbound / Triggered Delivery
  participant cancel_auth_blocked_run
  participant TurnCoordinator
  participant BlockedAuthFlowCanceller
  participant flow_record_source
  participant flow_manager

  SlackInbound->>cancel_auth_blocked_run: scope, gate_ref, auth_flow_canceller
  cancel_auth_blocked_run->>TurnCoordinator: cancel_run (idempotent)
  TurnCoordinator-->>cancel_auth_blocked_run: Ok(...)
  alt canceller present and gate_ref provided
    cancel_auth_blocked_run->>BlockedAuthFlowCanceller: cancel_blocked_auth_flow(scope, owner_user_id, run_id, gate_ref)
    BlockedAuthFlowCanceller->>flow_record_source: flow_for_turn_gate(include_terminal: false)
    flow_record_source-->>BlockedAuthFlowCanceller: Some(flow) | None
    alt non-terminal flow found
      BlockedAuthFlowCanceller->>flow_manager: cancel_flow(flow_id)
      flow_manager-->>BlockedAuthFlowCanceller: Ok(()) or terminal race
    end
    BlockedAuthFlowCanceller-->>cancel_auth_blocked_run: Ok(()) best-effort
  end
  cancel_auth_blocked_run-->>SlackInbound: cancel_run result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • nearai/ironclaw#4946: Both update Slack delivery auth-denial path; this PR's canceller wiring extends that same flow.
  • nearai/ironclaw#4953: Both modify triggered BlockedAuth cancellation in slack_delivery.rs; this PR adds durable flow cancellation at the same execution points.

Poem

A flow left Pending, the run long gone—
State drift at midnight, auth records drawn,
Now cancel_flow rides before cancel_run 🦀
Three denial paths converge as one,
The gate ref threads through, the race is won ✅

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows Conventional Commits style with 'fix(reborn): ...' prefix and accurately summarizes the core change.
Description check ✅ Passed Description comprehensively covers Problem/Fix/Tests/Review sections; all required validation items (fmt, clippy) confirmed; security review documented; linked issue cited.
Linked Issues check ✅ Passed PR fully addresses #4952: BlockedAuthFlowCanceller threaded through all three cancellation sites (live observer, triggered non-OAuth, OAuth backstop); race/error handling specs met.
Out of Scope Changes check ✅ Passed All changes directly scope to #4952: new trait, implementation in RebornProductAuthServices, wiring in runtime/slack_delivery/slack_host_beta, tests. No drift into e2e harness scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69d0734620

ℹ️ 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".

Comment on lines +1166 to +1170
if let (Some(canceller), Some(gate_ref)) = (auth_flow_canceller, gate_ref) {
let owner_user_id = scope.explicit_owner_user_id().unwrap_or(&actor.user_id);
if let Err(error) = canceller
.cancel_blocked_auth_flow(scope, owner_user_id, run_id, gate_ref)
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer AuthFlow cancel until cancel_run succeeds

When cancel_run fails after these lines succeed (the OAuth backstop already treats cancel failure as possible and deliberately leaves the Slack prompt in place), the run remains blocked but its backing AuthFlow has already been marked canceled, so the user can no longer complete the still-visible auth prompt and a retry sees no active flow. Keep the flow cancel after a successful run cancellation, or otherwise avoid making the auth flow terminal when the run cancellation did not take effect.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 315b850. Reordered cancel_auth_blocked_run to cancel the run FIRST and cancel the AuthFlow only after cancel_run succeeds (it's idempotent via slack-auth-block:{run_id}). A failed run-cancel now returns early and leaves both the flow and the still-usable prompt intact — the OAuth backstop relies on that. Regression: blocked_auth_cancel_run_failure_leaves_auth_flow_intact (cancel_run fails → recorder asserts zero flow-cancel calls).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@crates/ironclaw_reborn_composition/src/auth.rs`:
- Around line 1425-1426: The map_err closure in the AuthGateRef::new() call is
discarding the underlying error cause with |_| and using
AuthProductError::BackendUnavailable which is semantically incorrect for input
validation failures. Replace the map_err(|_|
AuthProductError::BackendUnavailable) pattern with map_err that captures the
actual error and converts it to AuthProductError::InvalidRequest with the reason
field populated from the error message. Apply this same fix to all occurrences
mentioned in the comment (including lines around 1444-1445).

In `@crates/ironclaw_reborn_composition/src/slack_delivery.rs`:
- Around line 1159-1179: Reorder the AuthFlow cancellation to occur after the
run cancellation to prevent state drift when run cancellation fails. Currently,
the if block checking auth_flow_canceller and gate_ref that calls
cancel_blocked_auth_flow executes before the run is cancelled, so if cancel_run
fails, the run remains in BlockedAuth state while its backing flow is already
terminal. Move this entire best-effort flow cleanup block to execute after a
successful run cancellation attempt, ensuring the run reaches a terminal state
first. Additionally, add a regression test using cancel_should_fail that
verifies no flow cancellation occurs when the run cancellation fails.
- Around line 4473-4483: The cancel_blocked_auth_flow method signature accepts
four key parameters (scope, owner_user_id, run_id, and gate_ref) but the test
double only records run_id and gate_ref when pushing to self.calls, dropping the
scope and owner_user_id arguments. Modify the data structure that self.calls
holds to capture all four arguments instead of just two, update the push
statement in cancel_blocked_auth_flow to include scope and owner_user_id
alongside run_id and gate_ref, and add assertions in at least one live test that
verify the recorded scope and owner_user_id match the expected values to prevent
regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8d4e8f91-6ce7-4cd5-9a8f-df4777e42986

📥 Commits

Reviewing files that changed from the base of the PR and between 78c196f and 69d0734.

📒 Files selected for processing (7)
  • crates/ironclaw_reborn_composition/src/auth.rs
  • crates/ironclaw_reborn_composition/src/auth_prompt.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/src/slack_delivery.rs
  • crates/ironclaw_reborn_composition/src/slack_host_beta.rs
  • crates/ironclaw_reborn_composition/src/slack_serve/e2e_tests.rs

Comment thread crates/ironclaw_reborn_composition/src/auth.rs Outdated
Comment thread crates/ironclaw_reborn_composition/src/slack_delivery.rs Outdated
Comment thread crates/ironclaw_reborn_composition/src/slack_delivery.rs Outdated

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (multi-agent)

Intent: Cancel stale Slack auth flows when auto-denying blocked auth runs.

Stats: 2 findings (from 6 raw, 2 after verification/dedup) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 1.

Bugs

  1. Medium Flow is canceled before the run, so a run-cancel failure strands state (crates/ironclaw_reborn_composition/src/slack_delivery.rs:1159-1179, confidence 84) — anchor: crates/ironclaw_reborn_composition/src/slack_delivery.rs:1159 (already open: #5014 (comment))

    cancel_auth_blocked_run clears the durable auth-flow record before it attempts cancel_run. If cancel_run fails, the run can remain blocked while the backing auth flow is already terminal, so the still-live auth prompt can no longer be completed and a retry no longer sees an active flow.

Conventions

  1. Medium New flow-cancel path drops parse errors with map_err(|_| ...) (crates/ironclaw_reborn_composition/src/auth.rs:1425-1445, confidence 100) — anchor: .claude/rules/error-handling.md:16-32

    The new cancel_blocked_auth_flow implementation maps both AuthGateRef::new(...) and TurnRunRef::new(...) failures to a generic BackendUnavailable with map_err(|_| ...). The repo error-handling rule explicitly rejects this shape because it drops the underlying cause.

run_id: TurnRunId,
gate_ref: &str,
) -> Result<(), AuthProductError> {
let gate_ref = AuthGateRef::new(gate_ref.to_string())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — New flow-cancel path drops parse errors with map_err(|_| ...).

The new cancel_blocked_auth_flow implementation maps both AuthGateRef::new(...) and TurnRunRef::new(...) failures to a generic BackendUnavailable with map_err(|_| ...). The repo error-handling rule explicitly rejects this shape because it drops the underlying cause.

Fix: Preserve the bound error with an error constructor that carries/logs the source, or log the bound error before converting to BackendUnavailable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 315b850 — both parses in cancel_blocked_auth_flow now use InvalidRequest { reason: format!("…: {err}") } preserving the source error.

…review)

Address PR review:
- ORDERING (codex P2 / coderabbit Major): cancel the run FIRST, then cancel the
  durable AuthFlow only on a successful/idempotent cancel_run. Cancelling the flow
  first meant a failed cancel_run left the run BlockedAuth with a terminal flow —
  the inverse state drift this PR fixes, and it broke the OAuth backstop contract
  that a failed cancel leaves the prompt usable. Regression test:
  blocked_auth_cancel_run_failure_leaves_auth_flow_intact (cancel_run fails →
  recorder asserts NO flow cancellation).
- ERROR CAUSE (coderabbit/henry): replace map_err(|_| BackendUnavailable) on the
  new AuthGateRef/TurnRunRef parses with AuthProductError::InvalidRequest{reason}
  carrying the bound error.
- TEST DOUBLE (coderabbit): RecordingBlockedAuthFlowCanceller now captures all four
  args (scope, owner_user_id, run_id, gate_ref); live + triggered tests assert the
  resolved owner_user_id and scope, not just run_id/gate_ref.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5014 June 17, 2026 06:30 Destroyed

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (multi-agent)

Intent: Cancel stale AuthFlow records when Slack auth auto-denies a blocked run, while keeping run cancellation behavior intact.

Stats: 3 findings (from 5 raw, 3 kept after validation) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 1.

Conventions

  1. Medium cancel_flow terminal race can still surface as an error (crates/ironclaw_reborn_composition/src/auth.rs:1459-1462, confidence 90) — anchor: crates/ironclaw_reborn_composition/src/auth.rs:1460
    The new canceller is documented to treat an already-terminal flow as a graceful no-op, but cancel_flow can still return Canceled or FlowAlreadyTerminal if the flow terminalizes after the read and before the cancel.

Tests

  1. Medium Best-effort auth-flow cancel failure is untested (crates/ironclaw_reborn_composition/src/slack_delivery.rs:1195-1207, confidence 91) — anchor: crates/ironclaw_reborn_composition/src/slack_delivery.rs:1195
    Existing tests cover successful canceller wiring and cancel_run failure, but not the branch where cancel_run succeeds and the best-effort flow cleanup fails and is swallowed.

Local Patterns

  1. Low Update the helper doc to mention flow cancellation (crates/ironclaw_reborn_composition/src/slack_delivery.rs:1145-1150, confidence 87) — anchor: crates/ironclaw_reborn_composition/src/slack_delivery.rs:1145 (no diff position — body only)
    The doc comment above cancel_auth_blocked_run still describes only run-level cancel_run behavior, but the function now also performs best-effort BlockedAuthFlowCanceller cleanup after the run succeeds.

return Ok(());
};
self.flow_manager
.cancel_flow(&flow.scope, flow.id)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — cancel_flow terminal race can still surface as an error.

The new canceller is documented to treat an already-terminal flow as a graceful no-op, but it first reads a non-terminal flow with include_terminal=false and then calls cancel_flow. If an OAuth callback or another canceller terminalizes the flow between those steps, cancel_flow returns Canceled or FlowAlreadyTerminal instead of Ok, so the documented no-op race contract is not actually upheld.

Fix: Normalize AuthProductError::Canceled and AuthProductError::FlowAlreadyTerminal from cancel_flow to Ok(()) in cancel_blocked_auth_flow, while still propagating real lookup/scope/backend errors.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3abeb20. cancel_blocked_auth_flow now normalizes AuthProductError::Canceled and AuthProductError::FlowAlreadyTerminal from cancel_flow to Ok(()), so a flow that terminalizes between the non-terminal read and the cancel honors the documented no-op race contract. Real lookup/scope/backend errors still propagate. Test cancel_blocked_auth_flow_treats_terminal_race_as_ok covers both terminal variants plus a negative case (BackendUnavailable still propagates).

.await?;

// Run is now terminal — cancel the stale `AuthFlow` record alongside it (#4952).
// Best-effort cleanliness: a flow-cancel failure does not surface, since the

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Best-effort auth-flow cancel failure is untested.

The new branch that swallows BlockedAuthFlowCanceller::cancel_blocked_auth_flow errors has no caller-level test. Existing tests cover successful canceller wiring and cancel_run failure, but not the intended best-effort case where cancel_run succeeds and flow cleanup fails without breaking Slack auto-denial.

Fix: Add tests::blocked_auth_canceller_failure_is_swallowed covering cancel_auth_blocked_run continuing after a canceller error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 3abeb20: blocked_auth_canceller_failure_is_swallowed — cancel_run succeeds, the BlockedAuthFlowCanceller returns Err, and the live observer path still cancels the run once and posts SLACK_AUTH_UNAVAILABLE_MESSAGE, proving the flow-cancel failure is swallowed without breaking auto-denial.

…wallow (PR #5014 review)

- TERMINAL RACE (review Medium): cancel_blocked_auth_flow read the flow with
  include_terminal:false then called cancel_flow; if the flow terminalized in
  between (concurrent OAuth callback / canceller), cancel_flow returned
  Canceled/FlowAlreadyTerminal, breaking the documented graceful-no-op contract.
  Normalize those two to Ok(()); real lookup/scope/backend errors still propagate.
  Test cancel_blocked_auth_flow_treats_terminal_race_as_ok (both terminal variants
  + a negative case proving BackendUnavailable still propagates).
- BEST-EFFORT SWALLOW (review Medium): add caller-level test
  blocked_auth_canceller_failure_is_swallowed — cancel_run succeeds, flow-cancel
  errors, auto-denial still cancels the run and posts the unavailable notice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5014 June 17, 2026 18:12 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@crates/ironclaw_reborn_composition/src/slack_delivery.rs`:
- Around line 4679-4764: The test `blocked_auth_canceller_failure_is_swallowed`
does not verify that the `cancel_blocked_auth_flow` method was actually invoked
on the `FailingBlockedAuthFlowCanceller`. Modify the
`FailingBlockedAuthFlowCanceller` struct to track invocation counts (using an
atomic counter or similar), increment the counter when
`cancel_blocked_auth_flow` is called, and add an assertion in the test to verify
that the canceller was invoked exactly once. This ensures the test will fail if
the code stops calling `cancel_blocked_auth_flow`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 531568d4-418d-4035-b48d-f43a03a84aa4

📥 Commits

Reviewing files that changed from the base of the PR and between 315b850 and 3abeb20.

📒 Files selected for processing (2)
  • crates/ironclaw_reborn_composition/src/auth.rs
  • crates/ironclaw_reborn_composition/src/slack_delivery.rs

Comment thread crates/ironclaw_reborn_composition/src/slack_delivery.rs

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (multi-agent)

Intent: Cancel stale AuthFlow records when Slack auth runs are auto-denied so the run and durable flow state stay aligned.

Stats: 4 findings (from 6 raw, 4 after filtering/dedup) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0.

Tests

  1. Medium Malformed gate refs in auth-flow cancel are untested (crates/ironclaw_reborn_composition/src/auth.rs:1425-1429, confidence 95) — anchor: crates/ironclaw_reborn_composition/src/auth.rs:1425
    cancel_blocked_auth_flow maps an invalid gate_ref into AuthProductError::InvalidRequest, but no test forces that parse failure.
  2. Medium Flow lookup failures are not covered (crates/ironclaw_reborn_composition/src/auth.rs:1438-1455, confidence 94) — anchor: crates/ironclaw_reborn_composition/src/auth.rs:1438
    The new flow_for_turn_gate(...).await? propagation is never forced to fail in tests.

Maintainability

  1. Medium Collapse the second flow-record facade (crates/ironclaw_reborn_composition/src/auth.rs:757-765, confidence 77) — anchor: crates/ironclaw_reborn_composition/src/auth.rs:757
    as_blocked_auth_flow_canceller repeats the same flow_record_source.is_some() gating already used by as_auth_challenge_provider.

Local Patterns

  1. Low Auth-flow canceller doc is narrower than the shared helper (crates/ironclaw_reborn_composition/src/slack_delivery.rs:130-134, confidence 78) — anchor: crates/ironclaw_reborn_composition/src/slack_delivery.rs:130
    The field comment frames auth_flow_canceller as only handling non-OAuth auth challenges, but the shared helper also handles the OAuth backstop path.

run_id: TurnRunId,
gate_ref: &str,
) -> Result<(), AuthProductError> {
let gate_ref = AuthGateRef::new(gate_ref.to_string()).map_err(|err| {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Malformed gate refs in auth-flow cancel are untested.

cancel_blocked_auth_flow maps an invalid gate_ref into AuthProductError::InvalidRequest, but no test forces that parse failure. If the gate-ref validation changes or stops surfacing the right error, this branch would go unverified.

Fix: Add tests::auth::cancel_blocked_auth_flow_rejects_invalid_gate_ref covering an invalid gate ref and asserting the InvalidRequest reason preserves the parse failure.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in a187119: cancel_blocked_auth_flow_rejects_invalid_gate_ref (empty gate_ref → AuthProductError::InvalidRequest with reason containing "invalid gate ref for auth-flow cancel").

// one) resolves to `None`, so the OAuth-callback race — where the flow
// completes just before auto-deny — is a graceful no-op rather than an
// error. We only ever cancel a flow that is still non-terminal.
let flow = source

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Flow lookup failures are not covered.

The new flow_for_turn_gate(...).await? propagation is never forced to fail in tests. A stub AuthFlowRecordSource that returns Err(BackendUnavailable) should be exercised so this helper does not accidentally swallow backend lookup failures.

Fix: Add tests::auth::cancel_blocked_auth_flow_propagates_flow_source_error using a failing AuthFlowRecordSource and asserting BackendUnavailable propagates.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in a187119: cancel_blocked_auth_flow_propagates_flow_source_error using a local AlwaysFailingFlowSource (flow_for_turn_gate → Err(BackendUnavailable)), asserting the lookup error propagates.

/// projection source. In that case the Slack path simply skips flow cancel and
/// still cancels the run, which is backward-compatible.
#[doc(hidden)]
pub fn as_blocked_auth_flow_canceller(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Collapse the second flow-record facade.

as_blocked_auth_flow_canceller repeats the same flow_record_source.is_some() gating already used by as_auth_challenge_provider, so RebornProductAuthServices now exposes two nearly identical optional facades over one underlying capability. That adds another public composition hop for Slack wiring without hiding new complexity.

Fix: Keep one auth-flow facade/accessor for flow-backed auth services, or have runtime composition derive both Slack handles from the same helper instead of adding a second near-identical accessor.

@henrypark133 henrypark133 Jun 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduped in 66a6830: extracted the shared flow_record_source.is_some() precondition behind a private has_flow_record_source() that both accessors call, so the gate can't drift. Kept them as two accessors rather than collapsing to one: they return distinct capability ports (AuthChallengeProvider vs BlockedAuthFlowCanceller), and the pre-existing as_auth_challenge_provider has several callers outside Slack (runtime ×2, DCR/factory tests) — a full collapse would churn those for no behavior gain. The only real duplication was the one-line gate, now defined once.

/// challenges are surfaced in Slack; other challenge kinds are denied (see the
/// `BlockedAuth` arm of `notification_for_actionable_state`).
pub auth_challenges: Option<Arc<dyn AuthChallengeProvider>>,
/// Cancels the durable `AuthFlow` record when a `BlockedAuth` run is auto-denied

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — Auth-flow canceller doc is narrower than the shared helper.

The field comment frames auth_flow_canceller as only handling Slack auto-deny of non-OAuth auth challenges, but the same handle is now threaded through the shared cancel_auth_blocked_run helper and used by the OAuth backstop too. That makes the comment stale for the next person touching this path.

Fix: Rewrite the comment to describe the shared blocked-auth cancellation contract across all callers, not just the non-OAuth Slack branch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a187119 — rewrote the auth_flow_canceller doc to describe the shared blocked-auth cancellation contract across all three callers (live non-OAuth deny, triggered non-OAuth deny, OAuth send-time backstop), cancelled after the run cancel succeeds.

… widen field doc (PR #5014 review)

- coderabbit: blocked_auth_canceller_failure_is_swallowed now asserts the failing
  canceller was actually invoked (call_count == 1) so the swallow test can't pass
  if the wiring stops calling cancel_blocked_auth_flow.
- review: add cancel_blocked_auth_flow_rejects_invalid_gate_ref (invalid gate_ref
  -> InvalidRequest{reason} preserving the parse context).
- review: add cancel_blocked_auth_flow_propagates_flow_source_error (flow_for_turn_gate
  backend error propagates, not swallowed).
- review: rewrite the auth_flow_canceller field doc to describe the shared
  blocked-auth cancellation contract across all three callers (live non-OAuth,
  triggered non-OAuth, OAuth send-time backstop), not just the non-OAuth branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5014 June 17, 2026 18:41 Destroyed
Extract the shared `flow_record_source.is_some()` precondition behind a private
`has_flow_record_source()` so the two flow-backed facades
(as_auth_challenge_provider / as_blocked_auth_flow_canceller) cannot drift. Kept
as two accessors since they expose distinct capability ports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5014 June 17, 2026 18:52 Destroyed

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review (multi-agent)

Intent: Cancel stale Slack auth flows when a blocked run is auto-denied, while keeping run cancellation behavior unchanged.

Stats: 2 findings (from 4 raw, 2 after filtering/dedup) across 1 file. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0

tests

  1. Medium Missing test for actor-user fallback on blocked-auth cancel (crates/ironclaw_reborn_composition/src/slack_delivery.rs:1169-1173, confidence 81) — anchor: crates/ironclaw_reborn_composition/src/slack_delivery.rs:1172
    The new owner-resolution branch in cancel_auth_blocked_run falls back to actor.user_id when scope.explicit_owner_user_id() is None, but the added Slack caller tests only assert the explicit-owner path. A regression here would break stale-flow cancellation for scopes without an explicit owner while the current tests would still pass.

local-patterns

  1. Low Use the module's named error field in the best-effort cancel log (crates/ironclaw_reborn_composition/src/slack_delivery.rs:1208-1212, confidence 93) — anchor: crates/ironclaw_reborn_composition/src/slack_delivery.rs:687-707
    This new debug log formats the failure as %error, while surrounding logs in slack_delivery.rs consistently use named fields such as error = %error. That drift makes the new cancellation log harder to filter with the rest of the module's structured tracing.

let flow_cancel_target = match (auth_flow_canceller, gate_ref) {
(Some(canceller), Some(gate_ref)) => {
let owner_user_id = scope
.explicit_owner_user_id()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Missing test for actor-user fallback on blocked-auth cancel.

The new owner-resolution branch in cancel_auth_blocked_run falls back to actor.user_id when scope.explicit_owner_user_id() is None, but the added Slack caller tests only assert the explicit-owner path. A regression here would break stale-flow cancellation for scopes without an explicit owner while the current tests would still pass.

Fix: Add tests::slack_delivery::blocked_auth_cancels_stale_auth_flow_falls_back_to_actor_user_when_scope_has_no_owner covering blocked-auth auto-deny with a scope that has no explicit owner.

tracing::debug!(
target = "ironclaw::reborn::slack_delivery",
%run_id,
%error,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — Use the module's named error field in the best-effort cancel log.

This new debug log formats the failure as %error, while surrounding logs in slack_delivery.rs consistently use named fields such as error = %error. That drift makes the new cancellation log harder to filter with the rest of the module's structured tracing.

Fix: Change the field to error = %error so the log matches the established structured-tracing style in this module.

@henrypark133
henrypark133 merged commit b718179 into main Jun 17, 2026
70 checks passed
@henrypark133
henrypark133 deleted the fix/slack-auth-stale-flow-4952 branch June 17, 2026 20:12
theredspoon pushed a commit to theredspoon/ironclaw that referenced this pull request Jun 21, 2026
…) (nearai#5014)

* fix(reborn): cancel stale AuthFlow on Slack auth auto-deny (nearai#4952)

When a Slack run blocked on interactive auth is auto-denied (non-OAuth
challenge the Slack surface can't satisfy, or an OAuth URL suppressed by
the send-time DM backstop), the delivery path cancelled the run directly
via TurnCoordinator but skipped AuthFlowManager::cancel_flow — unlike the
canonical AuthInteractionService deny path. The AuthFlow record lingered
non-terminal (Pending/AwaitingUser) until expiry. State-drift / cleanliness
gap, no security exposure.

Fix: add a narrow BlockedAuthFlowCanceller port (impl by
RebornProductAuthServices over its flow_record_source + flow_manager) and
thread it through the shared cancel_auth_blocked_run helper so the flow is
cancelled alongside the run at all three sites (live observer, triggered
non-OAuth arm, OAuth send-time backstop).

- include_terminal:false → already-terminal/Completed flows resolve to None
  and are a graceful no-op (handles the OAuth-callback race).
- Best-effort: a flow-cancel failure is debug-logged and does not block the
  run cancellation (the user-visible terminal action).
- None when no flow_record_source is wired (Slack without product-auth) —
  skips flow cancel, still cancels the run. Backward-compatible.
- Regression test drives the live observer caller with a recording fake
  canceller (test-through-the-caller).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(reborn): code-review fixes for nearai#4952 stale-auth-flow cancel

Address local review (bugs/security/maintainability/tests):
- Make cancel_auth_blocked_run's gate_ref an explicit Option<&str> and skip
  the flow cancel when absent, instead of passing an empty-string sentinel
  that silently no-ops inside the facade (was unwrap_or_default()).
- Add regression coverage for the two previously-untested call sites and the
  facade impl:
  * triggered_non_oauth_auth_cancels_stale_auth_flow (triggered non-OAuth arm)
  * triggered_oauth_backstop_cancels_stale_auth_flow (OAuth send-time backstop)
  * cancel_blocked_auth_flow_{cancels_non_terminal_flow,is_noop_when_flow_absent,
    is_noop_without_flow_record_source} (facade unit tests)
- Assert the cancelled run_id in blocked_auth_cancels_stale_auth_flow.

Security review: no findings (owner+gate scoped lookup, fail-closed terminal
guard, best-effort cancel does not block run cancellation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reborn): cancel AuthFlow only after cancel_run succeeds (PR nearai#5014 review)

Address PR review:
- ORDERING (codex P2 / coderabbit Major): cancel the run FIRST, then cancel the
  durable AuthFlow only on a successful/idempotent cancel_run. Cancelling the flow
  first meant a failed cancel_run left the run BlockedAuth with a terminal flow —
  the inverse state drift this PR fixes, and it broke the OAuth backstop contract
  that a failed cancel leaves the prompt usable. Regression test:
  blocked_auth_cancel_run_failure_leaves_auth_flow_intact (cancel_run fails →
  recorder asserts NO flow cancellation).
- ERROR CAUSE (coderabbit/henry): replace map_err(|_| BackendUnavailable) on the
  new AuthGateRef/TurnRunRef parses with AuthProductError::InvalidRequest{reason}
  carrying the bound error.
- TEST DOUBLE (coderabbit): RecordingBlockedAuthFlowCanceller now captures all four
  args (scope, owner_user_id, run_id, gate_ref); live + triggered tests assert the
  resolved owner_user_id and scope, not just run_id/gate_ref.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reborn): normalize terminal cancel_flow race + test best-effort swallow (PR nearai#5014 review)

- TERMINAL RACE (review Medium): cancel_blocked_auth_flow read the flow with
  include_terminal:false then called cancel_flow; if the flow terminalized in
  between (concurrent OAuth callback / canceller), cancel_flow returned
  Canceled/FlowAlreadyTerminal, breaking the documented graceful-no-op contract.
  Normalize those two to Ok(()); real lookup/scope/backend errors still propagate.
  Test cancel_blocked_auth_flow_treats_terminal_race_as_ok (both terminal variants
  + a negative case proving BackendUnavailable still propagates).
- BEST-EFFORT SWALLOW (review Medium): add caller-level test
  blocked_auth_canceller_failure_is_swallowed — cancel_run succeeds, flow-cancel
  errors, auto-denial still cancels the run and posts the unavailable notice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test+doc(reborn): cover facade error paths, assert canceller invoked, widen field doc (PR nearai#5014 review)

- coderabbit: blocked_auth_canceller_failure_is_swallowed now asserts the failing
  canceller was actually invoked (call_count == 1) so the swallow test can't pass
  if the wiring stops calling cancel_blocked_auth_flow.
- review: add cancel_blocked_auth_flow_rejects_invalid_gate_ref (invalid gate_ref
  -> InvalidRequest{reason} preserving the parse context).
- review: add cancel_blocked_auth_flow_propagates_flow_source_error (flow_for_turn_gate
  backend error propagates, not swallowed).
- review: rewrite the auth_flow_canceller field doc to describe the shared
  blocked-auth cancellation contract across all three callers (live non-OAuth,
  triggered non-OAuth, OAuth send-time backstop), not just the non-OAuth branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(reborn): dedup flow-backed facade gate (PR nearai#5014 review)

Extract the shared `flow_record_source.is_some()` precondition behind a private
`has_flow_record_source()` so the two flow-backed facades
(as_auth_challenge_provider / as_blocked_auth_flow_canceller) cannot drift. Kept
as two accessors since they expose distinct capability ports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5014 — 66a68308 Deployed Jun 17, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slack triggered/auth auto-deny leaves AuthFlow record stale (bypasses cancel_flow)

1 participant