Add bulk update for AI Tool Rules

What does this MR do and why?

Add bulk update for AI Tool Rules

How to set up and validate locally

  1. Enable the feature flag in the Rails console:
   Feature.enable(:gitlab_duo_governance_settings)
  1. Open the GraphQL explorer at http://gitlab.localdev:3000/-/graphql-explorer, and query the tool rules.
query {
  aiToolRules(fullPath: "gitlab-duo") {
    nodes {
      id
      name
      actionType
      category
      webAccess
      localAccess
      source
    }
  }
}
  1. Bulk update multiple tool rules:
   mutation {
     bulkUpdateAiToolRules(input: {
       fullPath: "gitlab-duo"
       toolRules: [
         { toolId: "gitlab_blob_search", webAccess: DENY, localAccess: DENY },
         { toolId: "ci_linter", webAccess: ASK, localAccess: DENY },
         { toolId: "run_git_command", webAccess: DENY, localAccess: DENY }
       ]
     }) {
       results {
         toolId
         errors
         toolRule {
           id
           webAccess
           localAccess
         }
       }
       errors
     }
   }

Expected: All three rules persisted, results returned with correct webAccess values, errors empty.

  1. Test partial failure with an unknown tool:
   mutation {
     bulkUpdateAiToolRules(input: {
       fullPath: "gitlab-duo"
       toolRules: [
         { toolId: "ci_linter", webAccess: ALLOW },
         { toolId: "invented_tool", webAccess: DENY }
       ]
     }) {
       results {
         toolId
         errors
         toolRule { id webAccess }
       }
       errors
     }
   }

Expected: ci_linter succeeds, invented_tool returns a per-item error, top-level errors is empty.

  1. Check the group audit log at http://gitlab.localdev:3000/groups/gitlab-duo/-/audit_events

    Expected: A single ai_tool_rules_bulk_updated audit event appears showing the tool count and tool names.

  2. Test project-scoped rules:

   mutation {
     bulkUpdateAiToolRules(input: {
       fullPath: "gitlab-duo"
       projectPath: "gitlab-duo/test"
       toolRules: [
         { toolId: "ci_linter", webAccess: DENY }
       ]
     }) {
       results {
         toolId
         toolRule { id webAccess }
       }
       errors
     }
   }

Expected: Rule is scoped to the project, namespace-level rule for create_issue is unaffected.

Re-running the first GraphQL query will return the namespace rules again.

Database queries

Bulk upsert (upsert_all)

INSERT INTO "ai_tool_rules" ("namespace_id","project_id","tool_name","web_access","local_access","tool_source","created_at","updated_at")
VALUES (1, NULL, 'create_issue', 0, NULL, 'gitlab', '2026-06-03 06:06:22.481055', '2026-06-03 06:06:22.481164')
ON CONFLICT ("namespace_id","project_id","tool_name")
DO UPDATE SET
  web_access = excluded.web_access,
  local_access = excluded.local_access,
  updated_at = excluded.updated_at
RETURNING "id"

Uses the idx_ai_tool_rules_ns_proj_tool_unique NULLS NOT DISTINCT unique index for conflict resolution. Single statement regardless of how many rules are submitted (up to 100).

Fetch saved rules (for_namespace + for_tool_names)

SELECT "ai_tool_rules".*
FROM "ai_tool_rules"
WHERE "ai_tool_rules"."namespace_id" = 1
  AND "ai_tool_rules"."project_id" IS NULL
  AND "ai_tool_rules"."tool_name" IN ('create_issue', 'read_file')

Execution plan: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/52145/commands/153599

Index Scan using idx_ai_tool_rules_ns_proj_tool_unique on public.ai_tool_rules
  (cost=0.12..3.15 rows=1 width=140) (actual time=0.630..0.630 rows=0 loops=1)
  Index Cond: ((namespace_id = 1) AND (project_id IS NULL) AND (tool_name = ANY ('{create_issue,read_file}'::text[])))
  Buffers: shared hit=3 read=1
Planning: 0.578 ms  Execution: 0.664 ms

Uses idx_ai_tool_rules_ns_proj_tool_unique index. Table is currently empty (pre-beta, gated behind gitlab_duo_governance_settings feature flag).

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.

Related to #601473

Edited by Jean van der Walt

Merge request reports

Loading