Safe Math Eval in Custom Tools

Evaluate user-supplied arithmetic in custom tools without exposing your agent to RCE

Use an AST allow-list instead of eval() when a custom tool evaluates user-supplied math.

python
from praisonaiagents import Agent, tool

@tool
def safe_calculator(expression: str) -> str:
    """Evaluate basic arithmetic safely (no code execution)."""
    from praisonaiagents.tools.safe_eval import safe_math_eval
    return str(safe_math_eval(expression))

agent = Agent(name="calc", tools=[safe_calculator])
agent.start("What is 15 + 27 * 3?")

The user supplies a maths expression; the tool evaluates it via an AST allow-list, not raw eval().

Diagram

Safe Math Evaluation

Pass

Reject

📝 Expression

🔍 AST Parse

✅ Allow-list Check

🧮 Compile/Eval

❌ Return Error

✅ Result

Quick Start

  1. Unsafe (don't do this)
  2. Safe pattern

    Use the AST-based allow-list for secure math evaluation:

    python
    import ast
    from praisonaiagents import Agent
    
    def _safe_calc(expr: str) -> str:
        """Safely evaluate basic arithmetic expressions using AST."""
        try:
            # Parse the expression into an AST
            tree = ast.parse(expr, mode='eval')
    
            # Check if all nodes are safe
            for node in ast.walk(tree):
                if not isinstance(node, (
                    ast.Expression, ast.BinOp, ast.UnaryOp,
                    ast.Constant, ast.Num,  # ast.Num for older Python versions
                    ast.Add, ast.Sub, ast.Mult, ast.Div,
                    ast.USub, ast.UAdd
                )):
                    return "error"  # Reject unsafe operations
    
            # Compile and evaluate the safe expression
            code = compile(tree, '<string>', 'eval')
            result = eval(code)
            return str(result)
        except Exception:
            return "error"
    
    # Create agent with safe calculator tool
    agent = Agent(
        name="MathAgent",
        instructions="You are a helpful math assistant. Use the calculator for arithmetic.",
        tools=[
            {
                "type": "function",
                "function": {
                    "name": "calculator",
                    "description": "Safely evaluate basic arithmetic expressions",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "expression": {
                                "type": "string",
                                "description": "Math expression like '2 + 3 * 4'"
                            }
                        },
                        "required": ["expression"]
                    }
                }
            }
        ]
    )
    
    @agent.tool
    def calculator(expression: str) -> str:
        """Safely calculate arithmetic expressions."""
        return _safe_calc(expression)
    
    # Test the safe calculator
    result = agent.start("Calculate 15 + 27 * 3")

How It Works

Diagram
AST ParserToolAgentUserAST ParserToolAgentUseralt[Safe expression][Unsafe expression]“Calculate 2 + 3 * 4”calculator(expression=”2 + 3 * 4″)ast.parse(expression)AST treeValidate nodes against allow-listcompile() and eval()“14”“The result is 14”“error”“Invalid expression”

Allow-list Reference

The AST allow-list permits only these node types for safe arithmetic evaluation:

Node TypePurposeExample
ast.ExpressionRoot expression nodeRequired wrapper
ast.BinOpBinary operations2 + 3, 4 * 5
ast.UnaryOpUnary operations-5, +3
ast.ConstantLiteral values42, 3.14
ast.AddAddition operator+
ast.SubSubtraction operator-
ast.MultMultiplication operator*
ast.DivDivision operator/
ast.USubUnary minus-x
ast.UAddUnary plus+x

Explicitly rejected constructs:

  • Function calls (pow(), exec(), __import__())
  • Attribute access (.attr, obj.method)
  • Names/variables (x, globals)
  • List/dict comprehensions
  • Power operator (**)
  • Modulo operator (%)

Common Patterns

Integrate with MCP server

python
from praisonaiagents.mcp import MCP
import ast

def _safe_calc(expr: str) -> str:
    """Safe math evaluation for MCP tools."""
    try:
        tree = ast.parse(expr, mode='eval')
        for node in ast.walk(tree):
            if not isinstance(node, (
                ast.Expression, ast.BinOp, ast.UnaryOp,
                ast.Constant, ast.Num,
                ast.Add, ast.Sub, ast.Mult, ast.Div,
                ast.USub, ast.UAdd
            )):
                return "error"
        code = compile(tree, '<string>', 'eval')
        return str(eval(code))
    except Exception:
        return "error"

# Use in MCP server context
mcp_server = MCP(
    name="safe-calculator",
    tools=[{
        "name": "calculate",
        "description": "Safely evaluate arithmetic expressions", 
        "handler": lambda expr: _safe_calc(expr["expression"])
    }]
)

Integrate with a local provider tool

python
from praisonai import Agent, ManagedAgent, LocalManagedConfig

def _safe_calc(expr: str) -> str:
    """AST-based safe calculator."""
    try:
        tree = ast.parse(expr, mode='eval')
        for node in ast.walk(tree):
            if not isinstance(node, (
                ast.Expression, ast.BinOp, ast.UnaryOp,
                ast.Constant, ast.Num,
                ast.Add, ast.Sub, ast.Mult, ast.Div,
                ast.USub, ast.UAdd
            )):
                return "error"
        code = compile(tree, '<string>', 'eval')
        return str(eval(code))
    except Exception:
        return "error"

def handle_calculator(tool_name, tool_input):
    """Safe calculator handler for managed agents."""
    expr = tool_input.get("expression", "0")
    result = _safe_calc(expr)
    print(f"  [Calculator: {expr} = {result}]")
    return result

# Configure managed agent with safe calculator
managed = ManagedAgent(
    provider="local",
    config=LocalManagedConfig(
        model="gpt-4o-mini",
        system="You are an assistant with a calculator tool. Use it for math.",
        name="SafeCalcAgent",
        tools=[{
            "type": "custom",
            "name": "calculator",
            "description": "Evaluate a math expression safely",
            "input_schema": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "Math expression"},
                },
                "required": ["expression"],
            },
        }],
    ),
    on_custom_tool=handle_calculator,
)

agent = Agent(name="calc", backend=managed)
result = agent.start("Use the calculator to compute 99 * 77")

Best Practices

Never call eval() on user input

Always parse user expressions through AST validation before evaluation. The eval() function can execute arbitrary Python code and should never receive untrusted input, even with restricted __builtins__.

Reject before parsing using a character allow-list

Add an initial character filter to catch obviously malicious input:

python
def _safe_calc(expr: str) -> str:
    # Pre-filter: only allow safe characters
    allowed_chars = set("0123456789+-*/(). ")
    if not all(c in allowed_chars for c in expr):
        return "error"

    # Continue with AST parsing...
    try:
        tree = ast.parse(expr, mode='eval')
        # ... rest of validation
    except Exception:
        return "error"
Return a sentinel string rather than raising

Return "error" instead of raising exceptions to provide graceful degradation in agent workflows. This prevents the entire agent conversation from failing due to a malformed math expression.

Cap expression length

Prevent resource exhaustion by limiting expression length:

python
def _safe_calc(expr: str) -> str:
    if len(expr) > 100:  # Reasonable limit
        return "error"

    # Continue with validation...

Related