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:
Alexander Whitestone
2026-04-12 23:54:23 -04:00
parent a0cd2c5338
commit 6719f8c307
11 changed files with 1803 additions and 0 deletions

View File

@@ -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"]