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
- Enable the feature flag in the Rails console:
Feature.enable(:gitlab_duo_governance_settings)- 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
}
}
}- 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.
- 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.
-
Check the group audit log at
http://gitlab.localdev:3000/groups/gitlab-duo/-/audit_eventsExpected: A single
ai_tool_rules_bulk_updatedaudit event appears showing the tool count and tool names. -
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 msUses 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