Redesign messaging adapter base with caller-resolves pattern

What does this MR do and why?

Redesigns the messaging adapter base class (Ai::Messaging::Adapters::Base) to provide a clean foundation for triggering AI flows from any messaging surface (Slack, GitLab note mentions, @GitLabDuo, future surfaces).

This replaces the approach in !234959 (closed) (which is being closed without merge) based on architectural feedback from that review. The three biggest changes:

  1. Adapters::Base#trigger becomes the single orchestration primitive. The concrete adapter (surface) owns all policy decisions — authorization, service account selection, flow selection, project selection — by implementing build_trigger_bundle. The shared Base#trigger handles only the mechanical work no surface should do for itself: composite identity linking, SA project membership, callback context enrichment, resource translation, and workflow execution. This eliminates the "two parallel orchestration paths" problem where RunService and TriggerFlowService both ended up at ExecuteWorkflowService but resolved and gated completely differently.

  2. TriggerBundle value object replaces symbol-keyed hashes. A Data.define-based struct with required-field validation catches contract violations at construction time inside the adapter, instead of with a deep NoMethodError inside ExecuteWorkflowService. The surface says: "Run this flow definition using this service account, in this project, against this resource" — and the type system enforces that contract.

  3. Tiered resilience replaces blanket safe {}. on_request_received (the user's acknowledgement) is wrapped in must {} which short-circuits on failure. Best-effort hooks like on_flow_started use safe {}. Security-critical steps (composite identity, SA membership, workflow execution) have no wrapper — they fail loudly. This fixes the silent-failure problem where a failed acknowledgement note would let the flow proceed, leaving the user with no visible response.

Additional changes:

  • TriggerFlowService deleted. Its namespace resolution, enablement check, and workspace project logic will be absorbed by concrete adapters' build_trigger_bundle implementations (out of scope for this MR).
  • CallbackWorker uses descendants-based registry. Adapters declare self.adapter_key; the registry is built from Base.descendants instead of a hardcoded hash. enriched_callback_context auto-injects the adapter key so adapters cannot accidentally write a mismatched string.
  • Single on_flow_failed hook with workflow: kwarg replaces the dual on_flow_failed/on_trigger_failed hooks. Sync failures pass workflow: nil; async failures pass the workflow object. Adapters branch on workflow.present? for conditional cleanup (e.g. Slack only removes the 👀 reaction when the workflow existed).
  • Composite identity linking uses Gitlab::Auth::Identity.link_from_web_request directly, per the review feedback on !234959 (closed) (!234959 (comment 3328524508)). No new auth-domain methods or parallel modules.

What this MR does NOT do

  • Does not add concrete adapters (Slack, GitlabNote) — those are separate MRs that stack on this.
  • Does not change ExecuteWorkflowService's public interface.
  • Does not refactor RunService — that remains a parallel path for non-adapter surfaces until a future consolidation.
  • Does not add a feature flag — the messaging adapter is not used in production yet.

References

How to set up and validate locally

This MR changes infrastructure that is not yet called from production code paths. Existing flows (RunService path) are untouched. Validation is via specs:

bundle exec rspec \
  ee/spec/services/ai/messaging/adapters/base_spec.rb \
  ee/spec/workers/ai/messaging/callback_worker_spec.rb

No changelog needed — internal infrastructure not yet used in production.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading