Draft: GitLab Duo in Slack: one Duo group per Slack workspace (full build, to be split)
Warning
Reference MR, not meant to be merged as is. This is the full end-to-end build of GitLab Duo in Slack v2. It gets split into smaller MRs before merge (.claude/skills/split-mr), see "Suggested split for the team" below. Use it as the reference for the design and for "this works end to end". The split MRs may still change details.
What does this MR do and why?
Today every GitLab Duo session started from Slack runs and bills in the default GitLab Duo namespace of whoever mentioned @GitLab. People in the same Slack workspace end up in different groups with different settings and credits ("works for me, not for you"), and nobody has one place to set it up. With this MR, each Slack workspace runs GitLab Duo in one top-level group, the Duo group. An Owner of that group chooses it, or the administrator on self-managed for an instance-wide install, and that group's settings, governance and credits then apply to everyone in the workspace.
Everything is behind the wip feature flag slack_duo_group, checked for the whole instance. With the flag off, master behavior applies: notifications and slash commands don't change, and the old default-namespace path works as before.
What's in it, by area:
- Table and model:
slack_workspaces(team_idunique,duo_namespace_idFK with cascade), modelIntegrations::SlackWorkspace::Workspace. - Flow registry:
requires_enablement: falseforslack_assistant/v1(only allowed for flows that run as the user on Workhorse). The flow is no longer a hidden second off switch. With the flag off, the group settings still list it. - Services, eligibility, audit, endpoints, presenters:
DuoGroupEligibility(one predicate for "may claim" and "claim still valid"),Enable/Disable/Release/ChangeDuoGroupService, audit eventsduo_in_slack_enabled/duo_in_slack_disabled, abilityupdate_duo_in_slack(Owner, top-level, not archived or scheduled for deletion),GET/PUT …/settings/slack/duo_group,PUT /admin/application_settings/slack/duo_group,DuoWorkspacesPresenter. - Mention path:
DuoMentionResolverpicks the Duo group of the workspace. The session runs there with the platform's own authorization (CreateWorkflowService, in practice Developer role or higher). Refusals become replies that name the setting and who can fix it. An unclaimed workspace gets "not set up yet, ask X", with no fallback. - Group and project UI: a GitLab Duo section above the unchanged notification form. It has a toggle for the page's own workspace, a requirements card with fixes, "Other Slack workspaces in this group" with "Show N more", and read-only pointers on subgroup and project pages.
- Claim on first install: the first install on a top-level group claims an unclaimed workspace. Reinstalls, subgroups, projects and copies never claim.
- Admin overview and picker (self-managed): every workspace on the instance, with a group picker for instance-installed workspaces.
- Install page picker: top-level groups you own above projects, with search.
- Docs: user docs (
gitlab_slack_application.md, "Choose where GitLab Duo runs"), development page (duo_in_slack.md), audit event docs.
Commits: one commit with the build, then small fixes on top: .metadata.yml for the new permission resource directory (master started requiring it while this branch was being built), the Pinia plugin in a Jest spec for Vue 3, and introduced_by_mr of the audit event types.
Suggested split for the team
This is a starting point, not a final plan (plan v2 §8). Each MR stays inert while the flag is off, so they can merge one by one.
| # | MR | Depends on | Can run in parallel with | Reviews Danger will ask for |
|---|---|---|---|---|
| 1 | Table and model | none | 2 | database |
| 2 | Flow registry requires_enablement |
none | 1 | backend |
| 3 | Services, eligibility, audit events, endpoints, presenter | 1 | 2 | backend, authorization (new ability) |
| 4 | Mention path | 2, 3 | 5, 6, 7 | backend, analytics (block_slack_duo_mention) |
| 5 | Group and project UI | 3 | 4, 6, 7 | frontend, UX |
| 6 | Claim on first top-level install | 3 | 4, 5, 7 | backend |
| 7 | Admin overview and picker (self-managed) | 3, shared components from 5 (slack_duo_requirements.vue, slack_duo_workspace_list.vue) |
4, 6 | frontend, backend |
| 8 | Install page picker and search | 3; reads best after 6 | 4, 5, 7 | frontend |
| 9 | Docs | the behavior it documents | anything | technical writing |
Once 1 and 3 are in, 4–8 can go to different people. Known follow-ups to pick up during the split are listed in the reviewer details below.
Open questions
| Question | Owner |
|---|---|
| Layout when one group has several Slack workspaces (main view plus "Other Slack workspaces in this group"): gitlab-org#23704 (comment 4022059112) | Nick (@nickleonard) |
Should GitLab Duo in Slack require "experiment and beta features" in the Duo group (GitLab.com) or the instance (self-managed)? It's built and can be removed in one place (DuoMentionResolver.experiment_features_required?) |
Roman (@rkolosovskiy) |
Cells: the claim is global by team_id, so a workspace connected to groups in two organizations has no single owner yet: #633611 |
us, with Cells |
EXPLAIN plans for the hierarchy queries (for_groups_in, for_projects_in, root_namespace_ids_by_team, installed_places). They go into the DB-touching split MRs (1 and 3) |
Author of split MRs 1 and 3 |
References
- Implementation issue: #633566
- Design discussion: gitlab-org#23704
- Feature flag rollout: #633582
- Cells follow-up (claim is global by
team_id): #633611 - Error messages and traceability: #632567 (closed)
- Interactive-run authorization (Guest/Reporter access, follow-up): #632568 (closed)
- Platform: consolidate server-side run starts: #629005
- Replaces the v1 stack (closed): !261830 (closed), !261845 (closed), !262156 (closed), !262157 (closed), !262158 (closed), !262159 (closed), !262160 (closed), !262161 (closed), !262165 (closed), !262171 (closed)
- Proof of concept, never merge: !261784 (closed)
Screenshots or screen recordings
All screenshots come from self-managed GDK with the test world seeded. Simulated GitLab.com looks the same, except for the instance-wide cases, which don't exist there.
| Before (flag off: master) | After (flag on) |
|---|---|
![]() |
![]() |
More states are in the steps below.
How to set up and validate locally
-
Seed the test world (users
sd-*, groups, and fake Slack installs in every state, so you don't need Slack). The seed scriptseed_duo_slack.rbis in this comment: !262856 (comment 4029700315). From yourgitlabdirectory:bin/rails runner -e development seed_duo_slack.rbIt enables
slack_duo_group,slack_duo_agentandslack_duo_api_flow. Run it again whenever you want to reset all GitLab Duo state. Run the web server as self-managed (noGITLAB_SIMULATE_SAAS) for the admin steps. Restartrails-webafter you change that. -
Turn it on. Sign in as
sd-ownerand open the GitLab for Slack integration page ofsd-a: http://gdk.test:3000/groups/sd-a/-/settings/integrations/gitlab_slack_application/edit. The GitLab Duo section sits above the unchanged notification form and is Off, with a toggle. Turn it on: the badge says On and the requirements are ready. On a new install, the first install on a top-level group does this for you. Reinstalls never do. -
Turn it off. Use the toggle and confirm. The claim is gone and an audit event
duo_in_slack_disabledis recorded. Turn it on again for the next steps. -
A second group with the same workspace.
sd-otherhas the same workspace installed. Assd-owner2, who can't seesd-a, its page says GitLab Duo runs in "another group". Assd-owner, the same page namessd-aand links to it.Viewer can't see the Duo group Viewer can see it 

-
Subgroup and project installs never turn GitLab Duo on. On
sd-sub, the top-level group gets the toggle. The subgroup page says who can turn it on and links there. -
Several workspaces in one group.
sd-multi's projects connect two workspaces, so each one gets a row with its own action.sd-manyhas 23: the page shows 3, then Show 20 more loads the rest, and a late row can still be turned on.sd-multisd-manyafter "Show 20 more"

-
Requirements say what's wrong and how to fix it. In
bin/rails console, runGroup.find_by_full_path('sd-a').namespace_settings.update!(duo_features_enabled: false)and reload: the card says what failed and links to the fix. Set it back totrueafterwards. A workspace without theapp_mentions:readSlack scope shows a warning with the reinstall action, and the badge never says On.Duo off in the group Missing Slack scope 

-
Self-managed: the administrator decides for an instance-wide install. As an administrator, open http://gdk.test:3000/admin/application_settings/integrations/gitlab_slack_application/edit. The overview lists every workspace on the instance. Group-installed ones are read-only, and the instance workspace gets a picker. Pick
sd-inst-a. As its Owner (sd-owner), the group page then shows the choice read-only.Admin, before Admin, after Owner of sd-inst-a


-
Install page. As
sd-owner, open http://gdk.test:3000/-/profile/slack/edit. Top-level groups you own come first, then projects, and search narrows both. On self-managed, picking a group while an instance-wide install exists shows a notice that the administrator decides.Group selected Search 

-
Mention path (Rails console, no Slack needed). Ask the resolver where a mention would run:
resolve = ->(team, username) do Integrations::SlackWorkspace::DuoMentionResolver.new(team_id: team, user: User.find_by_username(username)).execute end resolve.('T0WWA', 'sd-dev') # runs in sd-a (GitLab Duo on from step 2) resolve.('T0NOBODY', 'sd-dev') # not set up: the reply says who can set it upWith a real Slack app, mention
@GitLabin a workspace that has a Duo group. The session shows up in that group under Automate > Sessions. -
Flag off = master. Run
Feature.disable(:slack_duo_group)(the web process caches flags for about a minute). The Duo section, the admin overview and the group picker on the install page are gone. The Remove confirmation goes back to master's text, andslack_assistant/v1is listed in the group's GitLab Duo flow settings again and can be enabled. Enable the flag again afterwards.
Verification so far
- Walkthrough of every row in plan v2 §5. Self-managed: 43/43. Simulated GitLab.com: 36 passed, 7 skipped (self-managed-only rows).
- Mention-path harness: 21/21 in both modes.
- After the rebase onto current master, the Slack adapter, Duo group replies, flow registry and both
app_mentioned_servicespecs passed: 512 examples, 0 failures. The full suite runs in this pipeline. - rubocop, eslint, prettier, haml-lint, vale and markdownlint are clean on the changed files, and all lefthook pre-push checks pass.
Context for reviewer agents (plan, decisions, deviations, follow-ups, CE files)
Principles (plan v2 §2, short)
- Set up once, centrally. It then works for everyone. Users only connect their Slack account.
- One Slack workspace runs GitLab Duo in exactly one top-level group. That group's settings, governance and (on GitLab.com) credits apply.
- Whether a user can use it depends only on that user in that one group.
- Admins control it with switches that already exist ("Duo on", "Agent Platform on") plus one GitLab Duo toggle on the Slack integration. No flow, catalog or service-account setup.
- Duo and notifications are two parts of one integration. Changing where Duo runs never changes notifications.
- Duo is set up only at the top level. Subgroup and project installs stay for notifications and slash commands and never turn Duo on.
- A Slack session is an interactive run on behalf of the user: the user's token, Workhorse, chat's tool-approval rules, the Duo group as container, billing by flow name.
- The Slack code owns only which group and delivery. Everything else is a platform answer that Slack maps to a reply.
- GitLab.com: each customer's top-level group Owner chooses. Self-managed: the administrator chooses for instance installs. Credits there are instance-level.
- Self-managed sessions still run in a group, because the platform has no instance-level session container. That limits use to members of that group with the Developer role or higher.
Decisions (deliberate, flag them only for a real defect)
- Unit: one Duo group per
team_id, always top-level. One group may run several workspaces. No row means off. - Eligibility: if the workspace has an instance-level install in the group's organization, the administrator decides. Otherwise a top-level group may claim it when the workspace is installed anywhere in its hierarchy. The rule doesn't rely on
inherit_from_id, because that's unreliable for installs written into inheriting records. - Claim validity: the same predicate. Invalid claims (last install removed, group transferred, archived, scheduled for deletion) count as off and are deleted when found. There are no hooks in CE bulk deletes or transfer services.
- Claiming: only the first install on a top-level group claims. Existing installs from before the rollout stay off until someone turns Duo on, so there's no surprise billing.
- Second claim: rejected with "managed in another group". The group is named only if the viewer can read it.
- Moving Duo: only the self-managed admin picker can move it, in one transaction (
ChangeDuoGroupService). Owners turn it off in the old group first. - Unclaimed workspace: no fallback to the user's default namespace. The reply says who can turn it on.
- Flow:
slack_assistant/v1hasrequires_enablement: false. Only the sync always skips it. The seed and the enablement metric are as on master. With the flag on, the group flow settings don't list it. With the flag off, they list it as on master. No service account, no flow provisioning. This is a stopgap until sessions carry their own flow identity. - Authorization: the platform's check (
CreateWorkflowServicewith the group as container), in practice Developer role or higher. There's no Slack-sidemember?check. Guest and Reporter access is a follow-up (#632568 (closed)). - Experiment features: required in the Duo group (GitLab.com) or the instance setting (self-managed), shown on the card, and removable in one place (
DuoMentionResolver.experiment_features_required?). Open question for PM. - Feature flag: one
wipflag, no actor, so no claimed group ever falls back to the old path while others use the new one. Flag off means master behavior everywhere. - Notifications: unchanged. The Duo section sits above the unchanged notification form, with a static EE import in
integration_form.vue(placement waits for UX). - Cells: the claim is global by
team_id, with noCells::Claimableand noorganization_id. This is documented indb/docs/slack_workspaces.ymland the model comment, and tracked in #633611. - Paging: by
offset, one page per request (20 on group pages, 50 in the Admin area).QueryRecorderspecs keep the query count constant per page. - Refusal order: Duo off, Agent Platform off, no seat, then role (the role isn't named).
config/events/block_slack_duo_mention.ymlkeeps master'sproduct_group.- After the rebase: the adapter override now keeps master's refusal
reason(from "Tell Slack users why a Duo session could not start"). The Duo group path only adds which setting refused.DuoGroupReplies.textreturnsnilfor reasons it doesn't cover, so master's reply stays.
Shared CE files touched
app/models/slack_integration.rb:- scopes
instance_level,instance_level_in,for_groups_in,for_projects_in,excluding_teams,with_parent_routes,installed_places team_ids,team_names,root_namespace_ids_by_team,team_ids_with_scope
- scopes
app/services/integrations/slack_installation/base_service.rb: no-opafter_installhook that EE overrides.app/services/integrations/slack_installation/group_service.rb,app/services/integrations/slack_events/app_mentioned_service.rband the three Slack controllers:prepend_mod.- Install page:
app/helpers/integrations_helper.rb,app/views/profiles/slacks/edit.html.haml,app/assets/javascripts/integrations/gitlab_slack_application/*(newinstall_target_dropdown.vue, oldProjectsDropdownkept for flag off). app/assets/javascripts/integrations/edit/components/integration_form.vue,app/assets/javascripts/integrations/edit/index.js.config/authz/roles/owner.yml,config/authz/permission_groups/internal/group/archived.yml,config/authz/permissions/duo_in_slack/.config/events/block_slack_duo_mention.yml,db/structure.sql, generated routes.
Known follow-ups (pick up during the split)
- Audit event YAMLs:
introduced_by_mrstill points to v1 !261845 (closed) and should point to this MR or the split MR. - Frontend:
- The install page switches dropdowns on "groups empty" instead of an explicit boolean.
installable_groupsis unbounded and searched client-side.- The
groupspayload is snake_case. - Dead keys:
stateon the group page, admingroup_settings_path. SettingsSectionheading and search hook. The main title is an<h3>.- The toggle label should say the action.
- The Admin area can't reach rows past 50.
- The choice of REST over GraphQL still needs a justification.
- Backend:
- Put the
log_unclaimed(:managed_by_admin)rule in one place. for_teamstakes a scalar, and Release usesdelete_all.- Enable should rescue
RecordInvalid. - Drive the experiment setting in specs instead of stubbing
DuoRequirements. - Audit author when Enable releases a claim.
- Put the
- DB and security:
- Fold the FK migration.
team_xidnaming.- Name the
update_duo_in_slackresource for Govern:Authorization. - Deterministic dev fixture.
- "First install wins between two customers" needs a product decision.
- Validation:
- Walkthrough and harness case "GitLab Duo off in project P: a mention that reads an issue in P is refused" (promised in gitlab-org#23704).
- Platform:
- If the Slack flow moves to MCP tools, its token must keep the
ai_workflowsscope.
- If the Slack flow moves to MCP tools, its token must keep the
- Not 19.6:
- interactive-run authorization
- flow identity on sessions
- workspace entity (bot token and scopes in
slack_workspaces) - instance- or organization-level session container
- Slack
app_uninstalled/tokens_revoked - Cells routing
- Nick's collapsed notifications block
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.














