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
- Parent epic: Duo should remember command/tool approvals (gitlab-org#20519)
- Parent issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/602124+
- Pattern approval MR: !233672 (merged)
- Command validators MR: !239862 (merged)
How to set up and validate locally
Run the dedicated spec:
bin/rspec ee/spec/models/ai/duo_workflows/command_pattern_matcher_spec.rb65 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.