Skip to content

Commit 6b364bd

Browse files
authored
Merge pull request #436 from editor-code-assistant/agents-md-tool-args-matchers
Support regex patterns for shell_command in markdown agent tools
2 parents 7fbe51a + 0c798fa commit 6b364bd

4 files changed

Lines changed: 71 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- Bugfix: avoid `Divide by zero` crash in chat auto-compact when models.dev reports `0` for a model's context/output limits (e.g. `openai/chatgpt-image-latest`); such limits are now normalized to `nil` and `auto-compact?` skips models without a known positive context window.
1010
- Bugfix: image edit follow-up turns no longer fail on the OpenAI Responses API when prior generations are replayed; generated images are now persisted under a dedicated `image_generation_call` history role and replayed as a user-role `input_image` data URL across providers.
1111

12+
- Support regex patterns in markdown agent tool entries (e.g. `eca__shell_command(npm run .*)`) for fine-grained tool approval, currently limited to `eca__shell_command`.
13+
1214
## 0.130.1
1315

1416
- Add configurable skill paths and recursive directory loading for configured rules, commands, and skills; local skills are also discovered from `.agents/skills`. #423

docs/config/agents.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ Subagents can be configured in config or markdown and support/require these fiel
124124
You should run sleep 1 and return "I slept 1 second"
125125
```
126126

127+
!!! info "Pattern-based tool approval in markdown"
128+
129+
You can append a regex pattern in parentheses after a tool name to restrict approval to calls matching the pattern. Currently only `eca__shell_command` supports this — the pattern is matched against its `command` argument. Multiple entries for the same tool are automatically merged.
130+
131+
```yaml
132+
tools:
133+
allow:
134+
- eca__shell_command(npm run .*)
135+
- eca__shell_command(git diff(\s+.*)?)
136+
- eca__read_file
137+
```
138+
139+
This is equivalent to `argsMatchers` in JSON config. Patterns on tools other than `eca__shell_command` are currently ignored.
140+
127141
!!! info "Tool call approval"
128142
129143
For more complex tool call approval, use toolCall via config

src/eca/features/agents.clj

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,39 @@
1414

1515
(def ^:private logger-tag "[AGENTS-MD]")
1616

17+
(def ^:private tool-arg-name
18+
"Maps tool names to the argument name used for regex pattern matching in argsMatchers.
19+
Only tools that support pattern-based approval need an entry here."
20+
{"eca__shell_command" "command"})
21+
22+
(defn ^:private parse-tool-entry
23+
"Parses a tool entry string into [tool-name config].
24+
Plain names like 'eca__read_file' -> ['eca__read_file' {}]
25+
Pattern entries like 'eca__shell_command(npm run .*)' -> ['eca__shell_command' {:argsMatchers {'command' ['npm run .*']}}]"
26+
[entry]
27+
(let [s (str entry)]
28+
(if-let [[_ tool-name pattern] (re-matches #"(.+?)\((.+)\)" s)]
29+
(if-let [arg-name (get tool-arg-name tool-name)]
30+
[tool-name {:argsMatchers {arg-name [pattern]}}]
31+
(do (logger/warn logger-tag (format "Tool '%s' has pattern '%s' but no arg-name mapping in tool-arg-name; pattern will be ignored" tool-name pattern))
32+
[tool-name {}]))
33+
[s {}])))
34+
1735
(defn ^:private tools-list->approval-map
18-
[tool-names]
19-
(when (seq tool-names)
20-
(into {} (map (fn [name] [(str name) {}]) tool-names))))
36+
[tool-entries]
37+
(when (seq tool-entries)
38+
(reduce
39+
(fn [acc entry]
40+
(let [[tool-name config] (parse-tool-entry entry)]
41+
(if (contains? acc tool-name)
42+
;; Merge argsMatchers patterns for repeated tool entries
43+
(update-in acc [tool-name :argsMatchers]
44+
(fn [existing new-matchers]
45+
(merge-with into existing new-matchers))
46+
(:argsMatchers config))
47+
(assoc acc tool-name config))))
48+
{}
49+
tool-entries)))
2150

2251
(defn ^:private md->agent-config
2352
[{:keys [description mode model steps tools body inherit]}]

test/eca/features/agents_test.clj

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,29 @@
8484
:deny {"foo" {}}}}}
8585
config))))
8686

87+
(testing "tool entries with regex patterns"
88+
(let [parsed {:tools {"byDefault" "ask"
89+
"allow" ["eca__shell_command(npm run .*)"
90+
"eca__shell_command(git commit .*)"
91+
"eca__read_file"]}}
92+
config (#'agents/md->agent-config parsed)]
93+
(is (match? {:toolCall {:approval {:byDefault "ask"
94+
:allow {"eca__shell_command" {:argsMatchers {"command" ["npm run .*" "git commit .*"]}}
95+
"eca__read_file" {}}}}}
96+
config))))
97+
98+
(testing "tool entry with pattern for unknown tool arg (no tool-arg-name entry)"
99+
(let [parsed {:tools {"allow" ["eca__read_file(/tmp/.*)"]}}
100+
config (#'agents/md->agent-config parsed)]
101+
(is (match? {:toolCall {:approval {:allow {"eca__read_file" {}}}}}
102+
config))))
103+
104+
(testing "single tool with pattern"
105+
(let [parsed {:tools {"allow" ["eca__shell_command(git diff(\\s+.*)?)"]}}
106+
config (#'agents/md->agent-config parsed)]
107+
(is (match? {:toolCall {:approval {:allow {"eca__shell_command" {:argsMatchers {"command" ["git diff(\\s+.*)?"]}}}}}}
108+
config))))
109+
87110
(testing "minimal agent with only description and body"
88111
(let [parsed (shared/parse-md "---\ndescription: Simple agent\nmode: subagent\n---\n\nDo stuff")
89112
config (#'agents/md->agent-config parsed)]

0 commit comments

Comments
 (0)