fix(engines): normalize wildcard bash commands to canonical tool permission syntax#31599
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot all agentic engines should normalize wildcard bash command to the canonical syntax they sipport. Eg jq * -> shell(jq) |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…prefix form Add normalizeBashCommand() helper in engine_helpers.go that strips the " *" suffix from bash tool entries (e.g. "jq *" → "jq"). Apply it in all three agentic engines before wrapping the command in their respective permission formats: Copilot: shell(jq *) → shell(jq) Claude: Bash(jq *) → Bash(jq) Gemini: run_shell_command(jq *) → run_shell_command(jq) This means imported shared workflows like community-attribution.md (which use "jq *", "sed *", "cat *", etc.) now emit the canonical prefix form that Copilot CLI prefix-matching actually uses, without requiring every consumer to add redundant explicit bare entries. Updated test expectations and regenerated affected lock files. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in the latest commit. Added a shared
Imported shared workflows like |
There was a problem hiding this comment.
Pull request overview
Fixes tool-permission mismatches across Copilot/Claude/Gemini engines by normalizing bash tool entries written with a trailing " *" wildcard into canonical prefix-matching forms, preventing “Permission denied” for common commands imported from shared workflows.
Changes:
- Added
normalizeBashCommand()helper and unit tests to strip trailing" *"from per-command bash tool entries. - Applied normalization across Copilot, Claude, and Gemini tool permission generation + expanded engine test cases.
- Regenerated affected workflow lock files and removed an unused action pin entry.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/engine_helpers.go | Adds shared normalizeBashCommand() helper. |
| pkg/workflow/engine_helpers_test.go | Adds unit tests for the normalization helper. |
| pkg/workflow/copilot_engine_tools.go | Normalizes bash tool entries before emitting shell(...) allowlist items. |
| pkg/workflow/copilot_engine_test.go | Updates/extends Copilot engine tool permission tests for normalization. |
| pkg/workflow/claude_tools.go | Normalizes bash tool entries before emitting Bash(...) allowlist items. |
| pkg/workflow/claude_engine_tools_test.go | Adds Claude tool allowlist normalization test cases. |
| pkg/workflow/gemini_tools.go | Normalizes bash tool entries before emitting run_shell_command(...) items. |
| pkg/workflow/gemini_engine_test.go | Adds Gemini tool-core normalization test cases. |
| pkg/workflow/imports_inputs_test.go | Updates expectation to reflect normalized bash tool output in lock content. |
| pkg/workflow/data/action_pins.json | Removes an unused pinned action entry. |
| pkg/actionpins/data/action_pins.json | Removes an unused pinned action entry (mirrored dataset). |
| .github/aw/actions-lock.json | Removes an unused pinned action entry (GitHub-side lock). |
| .github/workflows/release.md | Adds explicit bash tool entries for the release workflow. |
| .github/workflows/release.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/workflow-skill-extractor.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/visual-regression-checker.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/unbloat-docs.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/stale-pr-cleanup.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/smoke-workflow-call.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/smoke-workflow-call-with-inputs.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/smoke-multi-pr.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/scout.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/safe-output-health.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/go-logger.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/draft-pr-cleanup.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/discussion-task-miner.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/dev-hawk.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/daily-syntax-error-quality.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/daily-safe-output-optimizer.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/daily-safe-output-integrator.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/daily-rendering-scripts-verifier.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/daily-geo-optimizer.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/daily-community-attribution.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/copilot-opt.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/copilot-cli-deep-research.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/blog-auditor.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
| .github/workflows/auto-triage-issues.lock.yml | Regenerated lock file reflecting normalized tool permissions. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 37/37 changed files
- Comments generated: 3
| func TestNormalizeBashCommand(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| expectedCmd string | ||
| expectedChanged bool | ||
| }{ | ||
| { | ||
| name: "plain command unchanged", | ||
| input: "jq", | ||
| expectedCmd: "jq", | ||
| expectedChanged: false, | ||
| }, | ||
| { | ||
| name: "command with space-star suffix is stripped", | ||
| input: "jq *", | ||
| expectedCmd: "jq", | ||
| expectedChanged: true, | ||
| }, | ||
| { | ||
| name: "multi-word command with space-star suffix is stripped", | ||
| input: "gh issue list *", | ||
| expectedCmd: "gh issue list", | ||
| expectedChanged: true, | ||
| }, | ||
| { | ||
| name: "command with arguments but no wildcard unchanged", | ||
| input: "jq . /tmp/file.json", | ||
| expectedCmd: "jq . /tmp/file.json", | ||
| expectedChanged: false, | ||
| }, | ||
| { | ||
| name: "lone star is not stripped (handled as full-wildcard sentinel)", | ||
| input: "*", | ||
| expectedCmd: "*", | ||
| expectedChanged: false, | ||
| }, | ||
| { | ||
| name: "colon-star is not stripped (handled as full-wildcard sentinel)", | ||
| input: ":*", | ||
| expectedCmd: ":*", | ||
| expectedChanged: false, | ||
| }, | ||
| { | ||
| name: "sed with space-star suffix is stripped", | ||
| input: "sed *", | ||
| expectedCmd: "sed", | ||
| expectedChanged: true, | ||
| }, | ||
| { | ||
| name: "awk with space-star suffix is stripped", | ||
| input: "awk *", | ||
| expectedCmd: "awk", | ||
| expectedChanged: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| gotCmd, gotChanged := normalizeBashCommand(tt.input) | ||
| if gotCmd != tt.expectedCmd { | ||
| t.Errorf("normalizeBashCommand(%q) cmd = %q, want %q", tt.input, gotCmd, tt.expectedCmd) | ||
| } | ||
| if gotChanged != tt.expectedChanged { | ||
| t.Errorf("normalizeBashCommand(%q) changed = %v, want %v", tt.input, gotChanged, tt.expectedChanged) | ||
| } | ||
| }) | ||
| } | ||
| } |
| // Add individual bash commands with Bash() prefix | ||
| for _, cmd := range bashCommands { | ||
| if cmdStr, ok := cmd.(string); ok { | ||
| allowedTools = append(allowedTools, fmt.Sprintf("Bash(%s)", cmdStr)) | ||
| // Normalize trailing " *" wildcard (e.g. "jq *" → "jq") so that | ||
| // all engines emit the canonical prefix form (Bash(jq)) regardless | ||
| // of whether the command was written with or without the wildcard. | ||
| normalized, _ := normalizeBashCommand(cmdStr) | ||
| allowedTools = append(allowedTools, fmt.Sprintf("Bash(%s)", normalized)) | ||
| } | ||
| } |
| cli-proxy: true | ||
| bash: | ||
| - jq | ||
| - bash |
Bug Fix
What was the bug?
The release agent (and other workflows importing
shared/community-attribution.md) was hitting repeated "Permission denied" errors forjq,awk,sed, andbashinvocations. The shared import provides these asjq *,sed *, etc., which compiled to--allow-tool shell(jq *). The Copilot CLI uses prefix matching —shell(jq *)only allows commands literally starting withjq *(literal asterisk), so every realjqinvocation was denied.How did you fix it?
Added a shared
normalizeBashCommand()helper inpkg/workflow/engine_helpers.gothat strips the trailing" *"suffix from bash tool entries, converting wildcard forms to their canonical prefix form:"jq *"→"jq""gh issue list *"→"gh issue list"Applied the normalization in all three agentic engines before they wrap the command in their engine-specific permission format:
shell(jq *)shell(jq)Bash(jq *)Bash(jq)run_shell_command(jq *)run_shell_command(jq)This means imported shared workflows like
community-attribution.md(which use"jq *","sed *","cat *", etc.) now emit the canonical prefix form that each engine's permission matcher actually uses, without requiring every consumer to add redundant bare entries.The full-wildcard sentinels
"*"and":*"(which signal "allow all shell commands") are handled separately and are unaffected by this change.Testing
TestNormalizeBashCommandunit tests for the new helper covering plain commands,cmd *patterns, multi-word commands, and sentinel values.shell(python3 *)form.shell(cmd *)patterns are replaced with canonicalshell(cmd)forms.