Add runner access column to tool governance UI
What does this MR do and why?
The tool governance UI currently lets admins set access rules for two categories of tool invocation — Web and Local — but has no way to govern tools called from flows running in CI/CD runners. As a result, runner-driven agent flows fall outside the governance model entirely: there's no UI surface to always-allow or always-deny tool usage originating from a runner.
The backend has already shipped the missing piece as part of https://gitlab.com/gitlab-org/gitlab/-/work_items/604842: a background_access column on ai_tool_rules, exposed over GraphQL as backgroundAccess, plus a dedicated :background surface in the tool-rules resolution service. The frontend simply hadn't caught up to reflect this third category. This MR is pure frontend — no backend changes.
Note on naming — "Runner" vs "background": the original issue and the UI use the term Runner, but during backend implementation the concept was renamed to background (the surface covers background flows generally, of which CI/CD-runner execution is the current case). So the user-facing column label is Runner access, while everything underneath — DB column, GraphQL field, governance surface — says
background. The mismatch is intentional and confined to the display label.
This MR adds that missing column to the governance table. The shared access-control component is generalized to support a third, backgroundAccess access type alongside webAccess/localAccess, but with only Always Allow and Always Deny available — Always Ask is intentionally omitted for the Runner column, since there's no human present in a CI/CD runner to respond to a prompt (that constraint is enforced at the model level on the backend). The GraphQL query and mutation are updated to read and write backgroundAccess, and the "carry over other access types" logic used when writing project-level overrides is generalized so it no longer silently drops whichever access type isn't currently being edited. The user-facing tool-governance doc, which claimed only two access categories exist, is corrected to match. While in the file, the inheritance badges' invalid variant="muted" (not a valid GlBadge variant; triggered Vue prop-validation warnings) is corrected to variant="neutral".
Closes https://gitlab.com/gitlab-org/gitlab/-/issues/606081. Parent epic: https://gitlab.com/groups/gitlab-org/-/work_items/21985.
References
Closes https://gitlab.com/gitlab-org/gitlab/-/issues/606081
Screenshots or screen recordings
How to set up and validate locally
- In rails console, run the exact fixture-setup script verbatim:
# frozen_string_literal: true # # Fixture for: "Add runner access column to tool governance UI" # # Mode: SaaS simulation (mode `saas` — the issue is scoped to the GitLab Duo # governance settings page, which per ee/lib/nav/gitlab_duo_settings_page.rb # is only shown when `gitlab_com_subscription?` is true, i.e. Gitlab.com? # returns true. This is SaaS-only surface, so this repro requires SaaS mode. # Switched via `bash /tmp/gdk-saas-mode.sh saas` before running this script.) # # RESET-FIRST + IDEMPOTENT: destroys any pre-existing state for the dedicated # repro group/project before recreating it, so re-running always yields an # identical starting state. # # What this creates: # - a dedicated group `cockpit-repro-tool-governance` with an Ultimate # GitlabSubscription (satisfies gitlab_duo_subscription_valid? without # relying on instance-wide Duo add-ons, matching the SaaS code path) # - a project inside that group # - a namespace-level Ai::ToolRule for tool `create_issue` with # background_access: 'deny' set (alongside web_access/local_access), # so the new Runner column has a non-default value to visibly display # # No pre-existing feature flag state is touched. `gitlab_duo_governance_settings` # is `default_enabled: true` and left at its default. The backend # `duo_workflow_background_tool_governance` flag (`wip`, default disabled) # gates only *runtime enforcement* of background/runner tool calls, not the # settings UI or the GraphQL read/write path being demoed here — left untouched. GROUP_PATH = 'cockpit-repro-tool-governance' PROJECT_NAME = 'repro-project' TOOL_NAME = 'create_issue' admin = User.admins.first || raise('no admin user found') # --- Reset: destroy any prior run's group (cascades project + tool rules) --- if (existing = Group.find_by_full_path(GROUP_PATH)) puts "setup_fixture: destroying pre-existing group #{GROUP_PATH} (id=#{existing.id})" existing.projects.each do |proj| ::Projects::DestroyService.new(proj, admin).execute end existing.destroy! end # --- Create the dedicated group, owned by admin --- group_result = Groups::CreateService.new(admin, name: 'Cockpit Repro Tool Governance', path: GROUP_PATH, organization_id: Organizations::Organization.default_organization.id ).execute raise "group creation failed: #{group_result.message}" unless group_result.success? group = group_result.payload[:group] group.add_owner(admin) # --- Give the group an Ultimate subscription (SaaS-mode semantics) --- plan = Plan.find_by(name: 'ultimate') || raise("ultimate plan not found - seed plans first") GitlabSubscription.find_or_create_by!(namespace: group) do |sub| sub.hosted_plan = plan sub.seats = 10 sub.start_date = Date.current sub.end_date = Date.current.advance(years: 1) end # --- Create a project inside the group --- project = ::Projects::CreateService.new(admin, { name: PROJECT_NAME, namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PRIVATE, initialize_with_readme: true }).execute raise "project creation failed: #{project.errors.full_messages}" unless project.persisted? # --- Create a namespace-level tool rule exercising all three access types --- rule = ::Ai::ToolRule.find_or_initialize_for_namespace(namespace_id: group.id, tool_name: TOOL_NAME) rule.web_access = 'allow' rule.local_access = 'ask' rule.background_access = 'deny' rule.save! puts "setup_fixture: group=#{group.full_path} (id=#{group.id})" puts "setup_fixture: project=#{project.full_path} (id=#{project.id})" puts "setup_fixture: tool_rule id=#{rule.id} tool_name=#{rule.tool_name} " \ "web_access=#{rule.web_access} local_access=#{rule.local_access} background_access=#{rule.background_access}" puts "setup_fixture: visit path -> /groups/#{group.full_path}/-/settings/gitlab_duo/governance" - Visit
/groups/cockpit-repro-tool-governance/-/settings/gitlab_duo/governance - Observe a Runner column now renders alongside Web and Local for the
create_issuerule, showing Always Deny (matching the fixture'sbackground_access: 'deny') - Confirm the Runner column's dropdown offers only Always Allow / Always Deny — no Always Ask option
- Change the Runner setting to Always Allow, reload the page, and confirm the change persisted (verifying
backgroundAccessis correctly read/written through the GraphQL API)
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.