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
-
Pick a namespace with no tool rules configured.
-
In a rails console, run the resolution service for the
:websurface and confirmpre_approved_toolsgoes from 75 entries to 80, gainingread_file,read_files,find_files,list_dirandgrep.Ai::ToolRules::ResolutionService.new(namespace: ns, surface: :web, project: nil).execute.payload -
Confirm
ask_toolsis empty both before and after. -
Check the governance settings page shows local file read tools as Always Allow rather than Always Ask for an unconfigured namespace.
Related
- 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.