feat(dap): Introduce command validators for tool calls

What does this MR do and why?

Resolves #602124

Adds program-specific command validators for pattern-based tool call approvals. The existing metacharacter guard (merged in !233672 (merged)) catches shell chaining attacks, but it cannot catch argument injection via program-specific flags that contain no shell metacharacters. This MR adds a second validation layer to close that gap.

The implementation introduces a CommandValidators framework with a GitValidator that enforces three layers of defense before allowing a pattern match:

  1. Subcommand allowlist (deny-by-default) -- only known-safe git subcommands are eligible for pattern approval
  2. Global option allowlist (deny-by-default) -- pre-subcommand flags must be in an explicit safe set
  3. Dangerous flag denylist (defense-in-depth) -- blocks known code-execution flags as an interim measure until per-subcommand flag allowlists are implemented (#602494)

Programs without a registered validator fail closed -- they require exact-match approval. This prevents argument injection in non-git programs that have no shell metacharacters.

Exact-match approvals are unaffected by all of the above -- the user explicitly approved that specific command.

References

Screenshots or screen recordings

N/A -- backend-only change, no UI impact.

How to set up and validate locally

Test via the GraphQL explorer (/-/graphql-explorer) or Rails console. Replace <ID> with your workflow's numeric ID.

1. Pattern approval for a safe git command (should approve)

mutation {
  updateDuoWorkflowToolCallApprovals(input: {
    workflowId: "gid://gitlab/Ai::DuoWorkflows::Workflow/<ID>"
    toolName: "run_command"
    pattern: "git *"
  }) {
    workflow { id toolCallApprovals }
    errors
  }
}

Then verify git checkout feature-branch is approved:

workflow = Ai::DuoWorkflows::Workflow.find(<ID>)
workflow.tool_call_approvals.approved?(
  tool_name: 'run_command',
  call_args: '{"command": "git checkout feature-branch"}'
) # => true

2. Argument injection is blocked (should reject)

workflow.tool_call_approvals.approved?(
  tool_name: 'run_command',
  call_args: '{"command": "git -c core.sshCommand=evil fetch"}'
) # => false (dangerous global flag)

3. Unregistered programs fail closed (should reject)

workflow.tool_call_approvals.add_pattern_approval(
  tool_name: 'run_command', pattern: 'curl *'
)
workflow.tool_call_approvals.approved?(
  tool_name: 'run_command',
  call_args: '{"command": "curl http://example.com"}'
) # => false (no registered validator for curl)

4. Run the specs

bundle exec rspec ee/spec/models/ai/duo_workflows/command_validators/git_validator_spec.rb
bundle exec rspec ee/spec/models/ai/duo_workflows/workflow_spec.rb

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.

Edited by Dylan Bernardi

Merge request reports

Loading
Loading