Default local file reads to allow, not ask

What does this merge request do and why?

DEFAULT_PREAPPROVED_GROUPS in ee/lib/ai/tool_rules/registry.rb gains one entry, :read_only_files, so it becomes %i[read_only_gitlab read_only_files]. This is the group default applied when a namespace has no explicit tool rule. With the change, local file reads (read_file, read_files, find_files, list_dir, grep) default to allow instead of ask. Writes, deletes and command execution are untouched and still default to ask.

Also updated: the default governance matrix row in doc/user/ai-governance/tool-governance.md, from "Read (local files) | Always Ask" to "Always Allow", and three spec files that pinned the old default.

This was agreed on #608968 by @nrosandich and @james.casey. The reasoning: sandboxing and in-project access controls are the right protection layer for file reads, not approval prompts, and #605706 only ever intended the ask default to cover write and destroy tools, not reads.

It also needs to land before the governance ceiling in ai-assist!6749. With the current default, the read tools sit in no allow list on any surface, so the ceiling strips them and reads start prompting everywhere, which is the incident in #622602 repeating.

Merge order only covers GitLab.com. Every cloud-connected instance talks to the same Duo Workflow Service, so a self-managed 19.3 or 19.4 keeps emitting the old allow list after the ceiling deploys there. What protects them: the ceiling is planned to ship behind duo_workflow_governance_ceiling, a Rails flag the service reads off gRPC metadata, so an instance that does not know the flag is never clamped (not built yet; acceptance criterion on #630713); this MR, !253700 (merged) and !257167 (merged) are backported to 19.4 and 19.3 (#630456) so those versions are ready when the flag is removed; and the service-side version gate (GitLab 19.5 or later) is the fallback if a backport is declined.

This supersedes !251850 (closed), which made the same change in August and was closed because it was framed as a bug fix rather than a policy decision. It is now a decision with sign-off.

Measured impact

Measured on GDK against a namespace with zero tool rules, duo_mcp_tool_governance on (its default). Absolute sizes depend on the catalog seed and on that flag, which adds the gitlab_ twin spelling for every pre-approved tool the MCP server serves; the deltas do not.

Surface Allow list Pre-approved groups Ask list
web +5 (read_file, read_files, find_files, list_dir, grep) [2, 7] to [2, 7, 8] empty before and after
ide +5, same tools [2, 7] to [2, 7, 8] empty before and after
background no change all eight before and after empty before and after

Background does not move because fallback_permission already runs the group default through Permissions.background_effective, which maps ask to allow since a pipeline has nobody to ask. The five read tools were already in its allow list before this change.

Why this cannot cause a new approval prompt

  • The resolved ask list is empty before this change and empty after. Measured on all three surfaces.
  • A group default never reaches the ask list. Only a rule an admin sets explicitly does.
  • So the change only ever adds to the allow list. Nothing moves into ask, and nothing is removed from anywhere.
  • ai-assist!6491 caused prompts by narrowing what was pre-approved. This widens. Opposite direction.

Known limitation this change inherits

resolve_group returns ask for a whole group as soon as one tool in it disagrees with the rest, so after this change a single Ask rule on grep drops the other four reads out of the allow list as well. This is existing behaviour and already applies to read_only_gitlab, where one Ask rule drops all 60. Nothing here makes it worse.

It is worth stating because it becomes load-bearing once ai-assist!6749 lands and the allow list starts clamping row-derived pre-approvals. The fix is small, allowed_tools_for_group already resolves per tool and just needs calling outside the when :allow branch, and it belongs with !6749 (closed) rather than here so the two land together.

Why there is no feature flag

Registry.default_permission_for is a pure class method taking only a tool or group name. It has no namespace or actor to gate on, so a percentage or namespace rollout would mean threading an actor through the registry and both of its callers, which is a larger change than the one being flagged. The change is one constant and a revert is a one-line change, so rollback does not need a flag.

Screenshots or screen recordings

Not applicable, no UI change.

How to set up and validate locally

  1. Pick a namespace with no tool rules configured.

  2. In a rails console, run the resolution service for the :web surface and confirm pre_approved_tools goes from 75 entries to 80, gaining read_file, read_files, find_files, list_dir and grep.

    Ai::ToolRules::ResolutionService.new(namespace: ns, surface: :web, project: nil).execute.payload
  3. Confirm ask_tools is empty both before and after.

  4. Check the governance settings page shows local file read tools as Always Allow rather than Always Ask for an unconfigured namespace.

  • Agreed on #608968.
  • Supersedes !251850 (closed).
  • Required before ai-assist!6749.
  • The GA plan in #606829 has been updated to match, so fail-closed there now covers write and execute only.

MR acceptance checklist

This change follows the MR acceptance checklist.

Edited by Pratyaksh Golash

Merge request reports

Loading
Loading