fix(skills): use standard CLI conventions for script argument passing

fix(skills): use standard CLI conventions for script argument passing Replace Anthropic-specific _skill_args injection with standard CLI argument conventions that work with any script regardless of origin. Changes: - Pass arguments via sys.argv (Python), process.argv (Node.js), and positional parameters (Shell) instead of proprietary global injection - Add arg_style parameter: "positional" (default), "named", or "env" - positional: sys.argv = ['script.py', 'val1', 'val2'] - named: sys.argv = ['script.py', '--key1', 'val1', '--key2', 'val2'] - env: only SKILL_ARG_* environment variables - Always set SKILL_ARG_* environment variables as fallback - Fix CodeExecution missing 'log' attribute by calling before_execution() - Update agent prompt documentation with new conventions and examples This enables skills_tool to work with standard CLI scripts that use sys.argv, argparse, click, or any other argument parsing method, rather than requiring scripts to be written specifically for Agent Zero.

TerminallyLazy committed Dec 30, 2025 at 16:28 UTC 5fefa5f53b4ca82cb80fd28419ba4363a002677a
2 files changed +150 -48
prompts/agent.system.tool.skills.md
+91 -28
@@ -119,25 +119,25 @@ Security:
119 ### 4. execute skill script
120
121 Executes bundled scripts from skill with arguments
122 -Scripts run in sandbox with injected arguments
122 +Scripts receive arguments via standard CLI conventions (sys.argv, process.argv)
123 Use when: skill provides script for deterministic operation or automation
124
125 ~~~json
126 {
127 "thoughts": [
128 - "Need to extract PDF form fields programmatically",
129 - "Skill provides extract_fields.py script",
130 - "Executing with target PDF path"
128 + "Need to convert PDF to images",
129 + "Skill provides convert_pdf_to_images.py script",
130 + "Script expects positional args: input_pdf output_dir"
131 ],
132 - "headline": "Executing PDF field extraction script",
132 + "headline": "Converting PDF to images",
133 "tool_name": "skills_tool",
134 "tool_args": {
135 "method": "execute_script",
136 "skill_name": "pdf_editing",
137 - "script_path": "scripts/extract_fields.py",
137 + "script_path": "scripts/convert_pdf_to_images.py",
138 "script_args": {
139 - "pdf_path": "/path/to/form.pdf",
140 - "output_format": "json"
139 + "input_pdf": "/path/to/document.pdf",
140 + "output_dir": "/tmp/images"
141 }
142 }
143 }
@@ -148,10 +148,20 @@ Required args:
148 - script_path: relative path to script file
149 - script_args: dictionary of arguments passed to script
150
151 -Supported script types:
152 -- .py (Python): args injected as _skill_args dictionary
153 -- .js (Node.js): args injected as _skill_args constant
154 -- .sh (Shell): args exported as environment variables
151 +Optional args:
152 +- arg_style: how to pass arguments to script (default: "positional")
153 + - "positional": values as positional args → sys.argv = ['script.py', 'value1', 'value2']
154 + - "named": as --key value pairs → sys.argv = ['script.py', '--key1', 'value1', '--key2', 'value2']
155 + - "env": only environment variables, no CLI args
156 +
157 +How scripts receive arguments:
158 +- .py (Python): sys.argv[1], sys.argv[2], etc. (standard argparse/CLI compatible)
159 +- .js (Node.js): process.argv[2], process.argv[3], etc. (standard CLI compatible)
160 +- .sh (Shell): $1, $2, etc. as positional parameters
161 +
162 +Environment variables (always available as fallback):
163 +- SKILL_ARG_KEY1=value1, SKILL_ARG_KEY2=value2, etc.
164 +- Scripts can use os.environ.get('SKILL_ARG_INPUT_PDF') if needed
165
166 Script execution:
167 - Runs in Docker container sandbox
@@ -159,6 +169,28 @@ Script execution:
169 - Returns stdout/stderr output
170 - Secure and isolated execution
171
172 +Example with argparse script (use arg_style="named"):
173 +~~~json
174 +{
175 + "thoughts": [
176 + "Script uses argparse with --input and --output flags",
177 + "Need to use named arg_style"
178 + ],
179 + "headline": "Running argparse-based script",
180 + "tool_name": "skills_tool",
181 + "tool_args": {
182 + "method": "execute_script",
183 + "skill_name": "data_processor",
184 + "script_path": "scripts/process.py",
185 + "script_args": {
186 + "input": "/path/to/data.csv",
187 + "output": "/tmp/result.json"
188 + },
189 + "arg_style": "named"
190 + }
191 +}
192 +~~~
193 +
194 ### 5. search skills by query
195
196 Searches skills by text matching in name, description, and tags
@@ -264,34 +296,38 @@ When script execution fails:
296
297 ## Examples
298
267 -Example 1: PDF form field extraction
299 +Example 1: Simple script with positional args (default)
300 +Script expects: python script.py /path/to/file.pdf
301 ~~~json
302 {
303 "thoughts": [
271 - "User has PDF form to analyze",
272 - "pdf_editing skill has extraction capabilities",
273 - "Will load skill and execute extraction script"
304 + "User has PDF to convert to images",
305 + "Script uses sys.argv[1] for input, sys.argv[2] for output",
306 + "Using default positional arg_style"
307 ],
275 - "headline": "Extracting PDF form fields",
308 + "headline": "Converting PDF to images",
309 "tool_name": "skills_tool",
310 "tool_args": {
311 "method": "execute_script",
312 "skill_name": "pdf_editing",
280 - "script_path": "scripts/extract_fields.py",
313 + "script_path": "scripts/convert_pdf_to_images.py",
314 "script_args": {
282 - "pdf_path": "/workspace/application.pdf"
315 + "input_pdf": "/workspace/document.pdf",
316 + "output_dir": "/tmp/images"
317 }
318 }
319 }
320 ~~~
321 +Result: sys.argv = ['script.py', '/workspace/document.pdf', '/tmp/images']
322
288 -Example 2: Web scraping with custom selector
323 +Example 2: Argparse script with named args
324 +Script expects: python script.py --url https://... --selector .price
325 ~~~json
326 {
327 "thoughts": [
328 "Need to scrape product prices from website",
293 - "web_scraping skill provides fetch script",
294 - "Using CSS selector to target price elements"
329 + "Script uses argparse with --url and --selector flags",
330 + "Using arg_style='named' for argparse compatibility"
331 ],
332 "headline": "Scraping product prices from webpage",
333 "tool_name": "skills_tool",
@@ -302,12 +338,38 @@ Example 2: Web scraping with custom selector
338 "script_args": {
339 "url": "https://example.com/products",
340 "selector": ".price"
305 - }
341 + },
342 + "arg_style": "named"
343 + }
344 +}
345 +~~~
346 +Result: sys.argv = ['script.py', '--url', 'https://...', '--selector', '.price']
347 +
348 +Example 3: Environment-only script
349 +Script reads from os.environ only
350 +~~~json
351 +{
352 + "thoughts": [
353 + "Script reads configuration from environment variables",
354 + "Using arg_style='env' to only set env vars"
355 + ],
356 + "headline": "Running config-based processor",
357 + "tool_name": "skills_tool",
358 + "tool_args": {
359 + "method": "execute_script",
360 + "skill_name": "data_processor",
361 + "script_path": "scripts/process.py",
362 + "script_args": {
363 + "input_file": "/data/input.csv",
364 + "mode": "production"
365 + },
366 + "arg_style": "env"
367 }
368 }
369 ~~~
370 +Result: SKILL_ARG_INPUT_FILE=/data/input.csv, SKILL_ARG_MODE=production
371
310 -Example 3: Data analysis workflow
372 +Example 4: Data analysis workflow
373 ~~~json
374 {
375 "thoughts": [
@@ -324,12 +386,12 @@ Example 3: Data analysis workflow
386 }
387 ~~~
388
327 -Then follow up with script:
389 +Then follow up with script (positional args):
390 ~~~json
391 {
392 "thoughts": [
331 - "Skill loaded, now analyzing CSV with grouping",
332 - "Using analyze_csv script with group_by parameter"
393 + "Skill loaded, now analyzing CSV",
394 + "Script takes csv_path as first arg, group_by as second"
395 ],
396 "headline": "Analyzing sales data grouped by category",
397 "tool_name": "skills_tool",
@@ -350,7 +412,8 @@ Then follow up with script:
412 - Skills metadata already loaded in your system prompt
413 - Skills cache after first load for efficiency
414 - Referenced files listed in load response
353 -- Scripts inject arguments as _skill_args variable
415 +- Scripts receive arguments via sys.argv (positional by default) + SKILL_ARG_* env vars
416 +- Use arg_style parameter to control argument passing: "positional", "named", or "env"
417 - All operations return formatted text responses
418 - Skills follow the open SKILL.md standard (cross-platform compatible)
419 - Use skills for structured procedures and contextual expertise
python/tools/skills_tool.py
+59 -20
@@ -20,7 +20,16 @@ class SkillsTool(Tool):
20 - search (query)
21 - load (skill_name)
22 - read_file (skill_name, file_path)
23 - - execute_script (skill_name, script_path, script_args)
23 + - execute_script (skill_name, script_path, script_args, arg_style)
24 +
25 + arg_style options for execute_script:
26 + - "positional" (default): Pass values as positional args (sys.argv[1], sys.argv[2])
27 + Example: {"input": "file.pdf", "output": "/tmp"} → sys.argv = ['script.py', 'file.pdf', '/tmp']
28 + - "named": Pass as --key value pairs (for argparse/click scripts)
29 + Example: {"input": "file.pdf", "output": "/tmp"} → sys.argv = ['script.py', '--input', 'file.pdf', '--output', '/tmp']
30 + - "env": Only use environment variables (SKILL_ARG_INPUT, SKILL_ARG_OUTPUT)
31 +
32 + Environment variables (SKILL_ARG_*) are always set regardless of arg_style.
33 """
34
35 async def execute(self, **kwargs) -> Response:
@@ -49,7 +58,9 @@ class SkillsTool(Tool):
58 script_args = kwargs.get("script_args") or {}
59 if not isinstance(script_args, dict):
60 script_args = {}
52 - return await self._execute_script(skill_name, script_path, script_args)
61 + # arg_style: "positional" (default), "named" (--key value), or "env" (env vars only)
62 + arg_style = str(kwargs.get("arg_style") or "positional").strip().lower()
63 + return await self._execute_script(skill_name, script_path, script_args, arg_style)
64
65 return Response(
66 message=(
@@ -176,7 +187,8 @@ class SkillsTool(Tool):
187 return f"File: {file_path}\n\n{text}"
188
189 async def _execute_script(
179 - self, skill_name: str, script_path: str, script_args: Dict[str, Any]
190 + self, skill_name: str, script_path: str, script_args: Dict[str, Any],
191 + arg_style: str = "positional"
192 ) -> Response:
193 if not skill_name:
194 return Response(message="Error: 'skill_name' is required for method=execute_script.", break_loop=False)
@@ -207,39 +219,64 @@ class SkillsTool(Tool):
219 script_runtime_path = str(script_abs)
220 script_runtime_dir = str(script_abs.parent)
221
210 - # Normalize args once for injection
211 - args_json = json.dumps(script_args, ensure_ascii=False)
222 + # Build environment variables (SKILL_ARG_*) - always set as fallback
223 + env_vars: Dict[str, str] = {}
224 + for k, v in (script_args or {}).items():
225 + env_key = f"SKILL_ARG_{re.sub(r'[^A-Za-z0-9_]', '_', str(k).upper())}"
226 + env_vars[env_key] = str(v)
227 +
228 + # Build CLI args based on arg_style:
229 + # - "positional": ['value1', 'value2'] - for scripts using sys.argv[1], sys.argv[2]
230 + # - "named": ['--key1', 'value1', '--key2', 'value2'] - for argparse/click scripts
231 + # - "env": [] - only use environment variables, no CLI args
232 + cli_args: List[str] = []
233 + if arg_style == "positional":
234 + cli_args = [str(v) for v in (script_args or {}).values()]
235 + elif arg_style == "named":
236 + for k, v in (script_args or {}).items():
237 + cli_args.append(f"--{k}")
238 + cli_args.append(str(v))
239 + # "env" style: cli_args stays empty, only env vars are used
240
241 if ext == ".py":
242 runtime = "python"
243 + # Set env vars (always available as fallback)
244 + env_lines = [f"os.environ[{json.dumps(k)}] = {json.dumps(v)}" for k, v in env_vars.items()]
245 + env_setup = "\n".join(env_lines) if env_lines else "pass"
246 + # Set sys.argv: ['script.py', ...cli_args]
247 + argv_list = [script_runtime_path] + cli_args
248 + argv_setup = f"sys.argv = {json.dumps(argv_list)}"
249 code = (
216 - "import json, os, runpy\n"
250 + "import os, sys, runpy\n"
251 f"os.chdir({json.dumps(script_runtime_dir)})\n"
218 - f"_skill_args = json.loads({json.dumps(args_json)})\n"
219 - f"runpy.run_path({json.dumps(script_runtime_path)}, run_name='__main__', init_globals={{'_skill_args': _skill_args}})\n"
252 + f"{env_setup}\n"
253 + f"{argv_setup}\n"
254 + f"runpy.run_path({json.dumps(script_runtime_path)}, run_name='__main__')\n"
255 )
256 elif ext == ".js":
257 runtime = "nodejs"
258 + # Set process.env (always available as fallback)
259 + env_lines = [f"process.env[{json.dumps(k)}] = {json.dumps(v)};" for k, v in env_vars.items()]
260 + env_setup = "\n".join(env_lines) if env_lines else ""
261 + # Node.js argv: ['node', 'script.js', ...cli_args]
262 + argv_list = ["node", script_runtime_path] + cli_args
263 code = (
224 - "const fs = require('fs');\n"
225 - "const path = require('path');\n"
264 f"process.chdir({json.dumps(script_runtime_dir)});\n"
227 - f"const _skill_args = JSON.parse({json.dumps(args_json)});\n"
228 - f"const __skill_script = fs.readFileSync({json.dumps(script_runtime_path)}, 'utf8');\n"
229 - "eval(__skill_script);\n"
265 + f"{env_setup}\n"
266 + f"process.argv = {json.dumps(argv_list)};\n"
267 + f"require({json.dumps(script_runtime_path)});\n"
268 )
269 elif ext == ".sh":
270 runtime = "terminal"
233 - env_parts: List[str] = []
234 - for k, v in (script_args or {}).items():
235 - key = re.sub(r"[^A-Za-z0-9_]", "_", str(k).upper())
236 - if not key:
237 - continue
238 - env_key = f"SKILL_ARG_{key}"
239 - env_parts.append(f"{env_key}={shlex.quote(str(v))}")
271 + # Environment variables (always available as fallback)
272 + env_parts = [f"{k}={shlex.quote(v)}" for k, v in env_vars.items()]
273 env_prefix = " ".join(env_parts)
274 + # Pass CLI args to script
275 + cli_args_str = " ".join(shlex.quote(a) for a in cli_args)
276 cd_cmd = f"cd {shlex.quote(script_runtime_dir)}"
277 run_cmd = f"bash {shlex.quote(script_runtime_path)}"
278 + if cli_args_str:
279 + run_cmd = f"{run_cmd} {cli_args_str}"
280 if env_prefix:
281 code = f"{cd_cmd} && {env_prefix} {run_cmd}"
282 else:
@@ -266,6 +303,8 @@ class SkillsTool(Tool):
303 loop_data=self.loop_data,
304 )
305
306 + # Must call before_execution to initialize self.log before execute()
307 + await cet.before_execution(**cet.args)
308 resp = await cet.execute(**cet.args)
309 # Wrap result to make it clear it was a skill script
310 wrapped = (