feat(tools): Constrain wildcard matching for tool approval patterns

What does this MR do and why?

Resolves: https://gitlab.com/gitlab-org/gitlab/-/work_items/602494+

Introduces a token-based CommandPatternMatcher for command tool approvals that constrains * wildcards from matching flag-shaped tokens (those starting with -). This closes the argument injection surface at the pattern matching layer rather than requiring per-program flag catalogs.

Previously, a pattern like git checkout * would match git checkout --upload-pack=evil because File.fnmatch treats * as matching any characters. Now, * matches exactly one non-flag token, so git checkout * matches git checkout main but rejects git checkout --upload-pack=evil.

Wildcard grammar

Token Semantics
* Matches exactly one token that does NOT start with -
** Matches zero or more tokens of any kind (including flags)
-- Literal end-of-options marker; after it, * matches any token
feature-* Embedded glob per-token, still rejects flag-shaped targets

Non-command tools (read_file, etc.) continue using File.fnmatch unchanged.

Key behavior changes

Pattern Command Before After
git checkout * git checkout main match match
git checkout * git checkout --force match no match
git checkout * git checkout branch extra match no match
git checkout -- * git checkout -- -weird-name match match

Why this approach

The security guarantee lives in the pattern grammar (* can't match flags), not in any per-program catalog that needs updating. This is zero-maintenance, universal across all tools, and composes cleanly with the existing CommandValidators framework.

References

How to set up and validate locally

Run the dedicated spec:

bin/rspec ee/spec/models/ai/duo_workflows/command_pattern_matcher_spec.rb

65 examples covering literals, *, **, --, embedded globs, edge cases, and security-critical injection scenarios.

Run the integration tests:

bin/rspec ee/spec/models/ai/duo_workflows/workflow_spec.rb -e "ToolCallApprovals"

109 examples covering the full approval pipeline including constrained wildcard behavior.

Manual GraphQL validation

Prerequisite: Tool approval for session enabled, existing workflow. Replace <ID> with your workflow ID.

1. Add a pattern approval

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

2. Verify matching (should return true)

query {
  duoWorkflowWorkflows(workflowId: "gid://gitlab/Ai::DuoWorkflows::Workflow/<ID>") {
    nodes {
      toolCallApproved(toolName: "run_command", toolCallArgs: "{\"command\":\"git checkout main\"}")
    }
  }
}

3. Verify flag rejection (should return false)

query {
  duoWorkflowWorkflows(workflowId: "gid://gitlab/Ai::DuoWorkflows::Workflow/<ID>") {
    nodes {
      toolCallApproved(toolName: "run_command", toolCallArgs: "{\"command\":\"git checkout --upload-pack=evil\"}")
    }
  }
}

4. Verify bare ** pattern rejection

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

Expected: "Wildcard-only patterns are not allowed for command tools"

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