Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
625 changes: 619 additions & 6 deletions crates/ironclaw_reborn_composition/src/auth.rs

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions crates/ironclaw_reborn_composition/src/auth_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,35 @@ pub trait AuthChallengeProvider: Send + Sync {
) -> Result<Option<AuthChallengeView>, AuthProductError>;
}

/// Cancels the durable `AuthFlow` record behind a blocked-auth turn gate.
///
/// When a Slack run blocked on interactive auth is auto-denied (a non-OAuth
/// challenge the Slack surface can't satisfy), the delivery path cancels the run
/// directly via `TurnCoordinator` rather than through the canonical
/// `AuthInteractionService` deny path (which *resumes* the run with a denied
/// disposition instead of cancelling it). Without this port the underlying
/// `AuthFlow` record lingers non-terminal (`Pending`/`AwaitingUser`) until it
/// expires — see issue #4952. Implemented by `RebornProductAuthServices` when a
/// `flow_record_source` is wired in; a no-op when it isn't.
///
/// Implementations MUST scope the lookup by caller user, run id, gate ref, and
/// tenant/agent/project/thread, and MUST treat an already-terminal (or absent)
/// flow as a graceful no-op so the OAuth-callback race — where the flow completes
/// just before auto-deny — does not surface an error.
#[async_trait]
pub trait BlockedAuthFlowCanceller: Send + Sync {
/// Cancel the non-terminal auth flow backing `(scope, run_id, gate_ref)`.
/// Returns `Ok(())` when the flow was cancelled, was already terminal, or
/// could not be found (nothing to cancel).
async fn cancel_blocked_auth_flow(
&self,
scope: &TurnScope,
owner_user_id: &UserId,
run_id: TurnRunId,
gate_ref: &str,
) -> Result<(), AuthProductError>;
}

pub(crate) async fn auth_prompt_view_for_blocked_auth(
fallback_owner_user_id: &UserId,
scope: &TurnScope,
Expand Down
2 changes: 1 addition & 1 deletion crates/ironclaw_reborn_composition/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ mod profile_approval_authorization;
mod project_filesystem_reader;
mod projection;
mod trajectory_observer;
pub use auth_prompt::{AuthChallengeProvider, AuthChallengeView};
pub use auth_prompt::{AuthChallengeProvider, AuthChallengeView, BlockedAuthFlowCanceller};
#[cfg(feature = "slack-v2-host-beta")]
mod delivered_gate_routing;
#[cfg(feature = "root-llm-provider")]
Expand Down
10 changes: 10 additions & 0 deletions crates/ironclaw_reborn_composition/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,16 @@ impl RebornRuntime {
.and_then(|product_auth| product_auth.as_auth_challenge_provider())
}

#[cfg(feature = "slack-v2-host-beta")]
pub(crate) fn blocked_auth_flow_canceller(
&self,
) -> Option<Arc<dyn crate::BlockedAuthFlowCanceller>> {
self.services
.product_auth
.as_ref()
.and_then(|product_auth| product_auth.as_blocked_auth_flow_canceller())
}

pub(crate) fn webui_event_stream(&self) -> Arc<dyn ProjectionStream> {
self.projection_services.webui_event_stream()
}
Expand Down
Loading
Loading