Push dap_for_each feature flag to the AI gateway

What

Adds the dap_for_each feature-flag definition and the push_feature_flag call that carries it to the AI gateway, so for_each can be exercised by real flows. The flag ships enabled by default — it is there to switch the mechanism off if it misbehaves, not to stage a rollout.

Why

ai-assist!6814 added the for_each fan-out component attribute on the gateway side, gated behind dap_for_each. A gateway-side flag needs both halves to work: the gate in the gateway, and the definition plus push_feature_flag here — the gateway only sees the flags the monolith sends it. !6814 (merged) landed the gateway half and left the monolith half as the deliberate next step; its own lib/feature_flags/context.py says so on the enum member:

Not yet defined in gitlab/config/feature_flags -- to be added alongside the first flow config that opts in.

This MR is that next step. The same comment sits on DAP_PARALLEL_SUBAGENTS four lines below, and that flag was defined in the monolith standalone anyway (!251134 (merged)), with no shipped flow config declaring parallel_subagents either — so landing the monolith half ahead of the first adopter is the established pattern in this family, not a departure from it.

Until this merges the gateway resolves dap_for_each as disabled, so a component declaring a for_each: block runs once, unwrapped, and the flow completes normally — the only trace is a gateway-side warning log.

Flag state — on by default

type: beta, default_enabled: true, rollout issue #630214.

  • Why on by default. for_each only changes behaviour for a component that explicitly declares a for_each: block; a component without one never reaches the flag check. No flow config shipped in ai-assist declares one today, so the flag being on is inert until a flow adopts the attribute — there is no incremental percentage worth ramping.
  • Why the flag exists. A central off-switch if a fan-out misbehaves. A fanned-out component runs its whole body per item (max_items default 1000, max_concurrency default 10), so the cost blast radius is real enough to want one.
  • Why not experiment, like its siblings. experiment, wip and gitlab_com_derisk cannot be default_enabled: true — Feature::Definition#validate_default_enabled! raises and Danger fails the MR.
  • Why beta and not ops. ops would also permit default-on and would drop the removal clock, but it is for long-lived operational controls; this flag is expected to be temporary and removed once for_each has been exercised by a real flow. beta says that out loud, at the cost of a rollout issue and a 6-month removal horizon. Happy to switch to ops if you would rather not carry the clock.
  • On Danger's "release the feature with the feature flag" note. The usual path is to ship a flag off, roll it out, then flip the default. This is a new flag, never set on any environment, so nothing is being flipped out from under an existing rollout and the default is what governs. No docs change is needed either — the "All feature flags in GitLab" page is generated from the YAML at docs-build time.

How

Wired exactly as its siblings (dap_parallel_subagents, dap_schema_auto_tool_choice, dap_workspace_agents), plus a spec asserting the push — which those siblings have and this one lacked. The push is user-scoped (current_user), matching all three siblings and sitting above this method's return unless root_namespace guard, so it is pushed on every DAP request rather than only those carrying a root namespace.

Scope

Not specific to any one flow — for_each is a platform primitive and any consumer needs this definition. It is deliberately not part of the BL Security Analyzer work, but BLSA depends on it: BLSA's move to the experimental component generation adopts for_each for its fan-out stages, so that work cannot land until this flag exists. !246889 (BLSA monolith integration) is the dependent MR and currently targets this branch. Product context: &23435.

Testing

  • The new spec in ee/spec/lib/api/helpers/duo_workflow_helpers_spec.rb fails on its assertion if the push_feature_flag call is removed — the stub accepts any arguments, so the example fails on have_received(...).with(:dap_for_each, user) rather than erroring. It also pins placement: it calls push_feature_flags with no arguments, so moving the line below the root_namespace guard or into the duo_developer branch would fail it too.
  • Pipeline 2869370151 is green on e194d572, zero failed jobs — including rspec:feature-flags and feature-flags-usage, which confirm the definition is matched to its usage and that its type agrees with its directory.

Known limitations

  • Flow configs do not only come from the ai-assist repo. "No shipped config declares for_each:" covers files in that repo. A customer-authored AI Catalog flow is passed through as flow_config (ee/app/services/ai/catalog/execute_workflow_service.rb), so a Catalog flow declaring for_each: runs once today and fans out once this merges. That is the intended capability, not a defect, but it is the concrete path worth knowing.
  • For self-managed, the off-switch is per-instance. With default_enabled: true the flag is on for self-managed too, and there is no central control there — disabling it is an instance-admin action, not something we can flip remotely.
  • The inert window has a known end. ai-assist!6322 (BLSA engine, still draft) adds flows/configs/bl_security/2.0.0-foreach.yml, the first shipped config to declare for_each:. When that lands, the fan-out becomes live behaviour rather than a dormant capability.
  • Turning the flag back off is not perfectly transparent. While on, an unknown key inside a for_each: block fails the flow at load time; while off the block is ignored and nothing inside it is validated. So disabling reverts an adopting component to a single unwrapped run rather than erroring.

Merge order

!246889 currently targets this branch, so this MR merges first and that one is retargeted to master after. Because of that stacking the branch is merged with master rather than rebased, so Danger warns about the master-merge commit's subject length and missing body. Squash-on-merge is on, so the landed commit takes the MR title and is unaffected.

⚠️ One trap for whoever merges !246889: its branch still carries the pre-move ee/config/feature_flags/experiment/dap_for_each.yml. Because this MR moves the definition to beta/, the two files sit at different paths and git reports no conflict — so !246889 must delete its copy before it merges, or master ends up with two definitions of dap_for_each and Feature::Definition raises. Tracked on that MR, not this one.

Edited by Meir Benayoun

Merge request reports

Loading
Loading