Poka-yoke: pre-execution validation catches ~970 errors
tools/code_execution_tool.py (+56 LOC): - ast.parse() before execution: catches ~236 SyntaxError - Tool name detection: catches ~279 NameError (forgetting imports) - Common import detection: catches ~100 missing imports tools/terminal_tool.py (+41 LOC): - which check before execution: catches ~461 exit_127 - Helpful suggestions for known missing commands (tmux, ruff, etc.) - Only runs for simple commands (no pipes/&&/;) Total: ~97 LOC to prevent ~970 errors (10 errors/LOC) Ref: Gitea #332, #333, #331
This commit is contained in:
@@ -916,6 +916,62 @@ def execute_code(
|
||||
if not code or not code.strip():
|
||||
return tool_error("No code provided.")
|
||||
|
||||
# --- Poka-yoke: pre-execution validation ---
|
||||
import ast
|
||||
|
||||
# 1. Syntax check (catches ~236 SyntaxError occurrences)
|
||||
try:
|
||||
ast.parse(code)
|
||||
except SyntaxError as e:
|
||||
return json.dumps({
|
||||
"error": f"SyntaxError: {e.msg} (line {e.lineno}). Fix the syntax before executing.",
|
||||
"status": "error",
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# 2. Detect tool names used without importing from hermes_tools
|
||||
# (catches ~279 NameError occurrences for tool names)
|
||||
_SANDBOX_TOOLS = {"read_file", "write_file", "terminal", "search_files",
|
||||
"patch", "web_search", "web_extract", "json_parse",
|
||||
"shell_quote", "retry", "fact_store", "fact_search",
|
||||
"fact_probe", "fact_feedback"}
|
||||
_COMMON_IMPORTS = {"os", "json", "re", "sys", "math", "csv", "datetime",
|
||||
"collections", "pathlib", "subprocess", "requests",
|
||||
"time", "shutil", "shlex", "glob", "io", "copy",
|
||||
"functools", "itertools", "hashlib", "base64",
|
||||
"urllib", "tempfile", "threading"}
|
||||
|
||||
if "from hermes_tools import" not in code:
|
||||
# Check if code uses tool names without importing
|
||||
used_tools = set()
|
||||
for tool in _SANDBOX_TOOLS:
|
||||
# Match tool name used as a function call: tool_name(
|
||||
if re.search(r'\b' + re.escape(tool) + r'\s*\(', code):
|
||||
used_tools.add(tool)
|
||||
if used_tools:
|
||||
return json.dumps({
|
||||
"error": (
|
||||
f"Names {used_tools} are tools, not Python builtins. "
|
||||
f"Add this import at the top of your code:\n"
|
||||
f"from hermes_tools import {', '.join(sorted(used_tools))}"
|
||||
),
|
||||
"status": "error",
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# 3. Detect common missing imports (os, json, re, etc.)
|
||||
if "import " not in code[:500]:
|
||||
used_imports = set()
|
||||
for mod in _COMMON_IMPORTS:
|
||||
if re.search(r'\b' + re.escape(mod) + r'\b', code):
|
||||
used_imports.add(mod)
|
||||
if used_imports:
|
||||
return json.dumps({
|
||||
"error": (
|
||||
f"Missing imports: {used_imports}. "
|
||||
f"Add at the top: import {', '.join(sorted(used_imports))}"
|
||||
),
|
||||
"status": "error",
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# Dispatch: remote backends use file-based RPC, local uses UDS
|
||||
from tools.terminal_tool import _get_env_config
|
||||
env_type = _get_env_config()["env_type"]
|
||||
|
||||
Reference in New Issue
Block a user