Governance tool rules (Ai::ToolRule) are not applied to Duo Agent Platform tool approvals on local surfaces (CLI / Chat v2)
Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.
Summary
Admin-configured governance rules (Ai::ToolRule, allow/ask/deny per tool) have no
effect on tool approval for Duo Agent Platform sessions started from local surfaces
(Duo CLI, Duo Chat v2 in the IDE). Repro: set run_command → Allow (local access)
at the group level, start a CLI or Chat v2 session, ask the agent to run a command —
the user is still prompted for approval.
Originally filed in gitlab-lsp as "wire up client-side governance fetch", and separately suspected to be an AI Gateway gap. Code tracing shows both framings are wrong: the gateway (Duo Workflow Service) is already fully wired to consume governance; the bug is in how Rails resolves and forwards the rules.
What already works (verified on master)
The gateway consumes governance through two channels:
- Privilege-level preapproval —
ToolsRegistrypreapproves every tool whose privilege group is in the workflow'spre_approved_agent_privileges_names(duo_workflow_service/components/tools_registry.py—configure()andapproval_required()), sourced from the Rails workflow record. - Per-tool JWT claim — Rails embeds
tool_access_policies: {allow, deny}into the cloud-connector token (ee/lib/ai/duo_workflow/duo_workflow_service/client.rb#cloud_connector_token), DWSGenerateTokenpropagates the claim (server.py_PROPAGATED_EXTRA_CLAIMS), andAbstractWorkflow._merge_jwt_governance_claims()applies it at workflow start (allow-list is the preapproval ceiling, deny-list is merged into denied tools).
Root causes (Rails)
1. Governance is resolved against the wrong surface
Ai::ToolRule has separate web_access / local_access columns and
ToolRule#access_for(surface) maps web/ambient → web_access, everything else
(ide, chat, chat_partial) → local_access. But both call sites that compute the
tool_access_policies JWT claim hardcode the surface to :web:
ee/app/services/ai/duo_workflows/workflow_context_generation_service.rb(resolve_tools,surface: :web)ee/lib/api/ai/duo_workflows/workflows.rb(get :wsdirect-access endpoint,surface: :web)
A rule configured for local access is therefore never resolved for the surfaces it
targets; resolution reads the (typically unset) web_access and falls back to ask.
Only CreateWorkflowService#build_resolution_service passes the real environment.
2. Client-supplied privileges bypass governance resolution
CreateWorkflowService#resolve_agent_privilegesreturns early when the client sendsagent_privileges— and both clients do: the CLI sendsagentPrivileges+preApprovedAgentPrivilegescomputed from the local agent mode (packages/cli/src/backend/gitlab/workflow_strategy.ts), Chat v2 sendspreApprovedAgentPrivileges: [].UpdateAgentPrivilegesService(used by the CLI on mode switch) writes client-sent privilege lists with no governance clamp.
Net effect: governance allow never reaches pre_approved_agent_privileges_names,
and a client can in principle widen ask to preapproved.
3. Gating conditions worth checking during repro
resolve_tools returns empty policies unless the request has a container and
Feature.enabled?(:gitlab_duo_governance_settings, container).
Proposed fix
- Pass the workflow's actual surface/environment into
Ai::ToolRules::ResolutionServiceat both JWT-claim call sites (executor token path and:wsdirect-access path). - In
CreateWorkflowService#resolve_agent_privilegesandUpdateAgentPrivilegesService, merge client-requested privileges with governance resolution instead of skipping it: governance deny/ask is a ceiling the client cannot widen; governance allow is applied as preapproval regardless of what the client requests.
No AI Gateway changes required. (Optional follow-up: a GOVERNANCE variant in the
ApprovalSource proto enum for audit clarity.)
Acceptance criteria
-
run_commandlocal_access = allow → CLI and Chat v2 auto-run commands, no prompt -
run_commandlocal_access = deny → tool blocked on both surfaces -
run_commandlocal_access = ask → prompt shown (current behavior) - Web/ambient surfaces unaffected (still resolved against
web_access) - Client-supplied
pre_approved_agent_privilegescannot widen a governanceask/deny
Out of scope / follow-up
Client-side consumption of aiToolRules (fetchGovernanceRules() scaffolding on
db/persistent-approvals-config-file) is a separate concern: per the delegation
direction, local policy answers governance ask (PreToolUse hooks, prerequisite
gitlab-org/editor-extensions/gitlab-lsp!3610) — it must not be the enforcement mechanism for allow/deny. Follow-up
issue in gitlab-lsp if still wanted.
Related
- Epic: &21877 (Persistent Approvals Across Sessions), under &20519
- gitlab-org/editor-extensions/gitlab-lsp!3610 (PreToolUse hooks)
- Moved from gitlab-org/editor-extensions/gitlab-lsp#2717 (moved) after investigation showed the defect is in Rails, not the LSP or AI Gateway.