Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Crewai-agents/trip_planner/tests/test_calculator_safety.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import importlib.util
from pathlib import Path
import pytest

# Dynamically load the calculator_tools module (handles hyphenated parent folder)
calc_path = Path(__file__).resolve().parents[1] / "tools" / "calculator_tools.py"
spec = importlib.util.spec_from_file_location("calculator_tools", str(calc_path))
calculator = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(calculator)
calculate = calculator.calculate


@pytest.mark.parametrize(
"expr,expected",
[
("2+3", 5),
("10*5", 50),
("100/4", 25.0),
("2**3", 8),
("(2+3)*4", 20),
("sqrt(16)", 4.0),
("sin(0)", 0.0),
("cos(0)", 1.0),
("tan(0)", 0.0),
("log10(100)", 2.0),
("abs(-10)", 10),
("round(3.14159, 2)", 3.14),
("pow(2, 8)", 256),
("pi", 3.141592653589793),
("e", 2.718281828459045),
],
)
def test_valid_expressions(expr, expected):
result = calculate(expr)
if isinstance(expected, float):
assert pytest.approx(expected, rel=1e-9) == result
else:
assert expected == result


@pytest.mark.parametrize(
"expr",
[
'import("os")',
'import("os").system("ls")',
'eval("2+2")',
'exec("print(1)")',
'open("/etc/passwd")',
'globals()',
'locals()',
'().__class__.__bases__[0]',
'lambda x: x',
'[1,2,3]',
'{"a":1}',
'compile("1+1", "", "eval")',
'True',
'False',
],
)
def test_malicious_inputs_rejected(expr):
with pytest.raises(ValueError):
calculate(expr)
135 changes: 123 additions & 12 deletions Crewai-agents/trip_planner/tools/calculator_tools.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,126 @@
from langchain.tools import tool
import ast
import math
from typing import Any

try:
from langchain.tools import tool
except Exception:
def tool(name: str):
def decorator(fn):
return fn
return decorator

class CalculatorTools:
@tool("Make a calculation")
def calculate(operation):
"""Useful to perform any mathematical calculations,
like sum, minus, multiplication, division, etc.
The input to this tool should be a mathematical
expression, a couple examples are `200*7` or `5000/2*10`
"""
# Whitelisted math functions and constants
_ALLOWED_FUNCS = {
"sqrt": math.sqrt,
"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"log": math.log,
"log10": math.log10,
"abs": abs,
"round": round,
"pow": pow,
}

_ALLOWED_CONSTS = {"pi": math.pi, "e": math.e}

_ALLOWED_BINOPS = {
ast.Add: lambda a, b: a + b,
ast.Sub: lambda a, b: a - b,
ast.Mult: lambda a, b: a * b,
ast.Div: lambda a, b: a / b,
ast.Mod: lambda a, b: a % b,
ast.Pow: lambda a, b: a ** b,
}

_ALLOWED_UNARYOPS = {ast.UAdd: lambda a: +a, ast.USub: lambda a: -a}


class _SafeEvaluator(ast.NodeVisitor):
def visit(self, node: ast.AST) -> Any:
node_type = type(node)
if node_type in (ast.Expression, ast.Module):
# expression wrapped as Module/Expression
body = getattr(node, "body", None)
if isinstance(body, list):
if len(body) != 1:
raise ValueError("Only single expressions are allowed")
return self.visit(body[0])
return self.visit(node.body)
return super().visit(node)

def visit_BinOp(self, node: ast.BinOp) -> Any:
op_type = type(node.op)
if op_type not in _ALLOWED_BINOPS:
raise ValueError(f"Operator {op_type.__name__} not allowed")
left = self.visit(node.left)
right = self.visit(node.right)
try:
return eval(operation)
except SyntaxError:
return "Error: Invalid syntax in mathematical expression"
return _ALLOWED_BINOPS[op_type](left, right)
except ZeroDivisionError:
raise ValueError("Division by zero")

def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
op_type = type(node.op)
if op_type not in _ALLOWED_UNARYOPS:
raise ValueError(f"Unary operator {op_type.__name__} not allowed")
operand = self.visit(node.operand)
return _ALLOWED_UNARYOPS[op_type](operand)

def visit_Call(self, node: ast.Call) -> Any:
# Only allow simple name calls (no attribute access)
if not isinstance(node.func, ast.Name):
raise ValueError("Only direct function calls are allowed")
func_name = node.func.id
if func_name not in _ALLOWED_FUNCS:
raise ValueError(f"Function '{func_name}' is not allowed")
# no keywords allowed
if node.keywords:
raise ValueError("Keyword arguments are not allowed")
args = [self.visit(arg) for arg in node.args]
return _ALLOWED_FUNCS[func_name](*args)

def visit_Name(self, node: ast.Name) -> Any:
if node.id in _ALLOWED_CONSTS:
return _ALLOWED_CONSTS[node.id]
raise ValueError(f"Use of name '{node.id}' is not allowed")

def visit_Constant(self, node: ast.Constant) -> Any:
if isinstance(node.value, bool):
raise ValueError("Boolean constants are not allowed")
if isinstance(node.value, (int, float)):
return node.value
raise ValueError("Only numeric constants are allowed")

# Reject lists, dicts, attributes, comprehensions, etc.
def generic_visit(self, node: ast.AST) -> Any:
raise ValueError(f"Unsupported expression: {node.__class__.__name__}")


@tool("Make a calculation")
def calculate(operation: str) -> Any:
"""Evaluate a mathematical expression safely.

Only numeric literals, arithmetic operators, parentheses, and
a small whitelist of math functions/constants are permitted.
"""
if not isinstance(operation, str):
raise ValueError("Operation must be a string")
expression = operation.strip()
if not expression:
raise ValueError("Empty expression")
try:
node = ast.parse(expression, mode="eval")
except SyntaxError as exc:
raise ValueError("Invalid syntax in mathematical expression") from exc
evaluator = _SafeEvaluator()
result = evaluator.visit(node)
# Keep numeric types only
if isinstance(result, (int, float)):
return result
raise ValueError("Expression did not evaluate to a numeric result")


class CalculatorTools:
calculate = calculate
84 changes: 67 additions & 17 deletions web3/internet-computer/scripts/devcontainer-setup.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,75 @@
#!/bin/bash
set -e
set -euo pipefail

# This script is Bash-specific and must be run with Bash.

echo "🚀 Setting up devcontainer..."

# Fix for git dubious ownership issue in Codespaces
git config --global --add safe.directory /workspaces/fetch-icp-integration
# Validate required tool: git
if ! command -v git >/dev/null 2>&1; then
echo "git is required but not found in PATH. Please install git." >&2
exit 1
fi

# Determine workspace root dynamically and mark it safe for git operations
if git rev-parse --show-toplevel >/dev/null 2>&1; then
WORKSPACE_ROOT=$(git rev-parse --show-toplevel)
else
WORKSPACE_ROOT="$PWD"
fi

git config --global --add safe.directory "$WORKSPACE_ROOT" || true

# Simple retry wrapper for transient network/install failures
retry() {
local -r max_attempts=3
local -r cmd=("$@")
local attempt=1
until "${cmd[@]}"; do
if [ "$attempt" -ge "$max_attempts" ]; then
echo "Command failed after $attempt attempts: ${cmd[*]}" >&2
return 1
fi
echo "Command failed, retrying (${attempt}/${max_attempts})..."
attempt=$((attempt + 1))
sleep $((attempt * 2))
done
}

# Install Azle CLI
# Validate required tools: node and npm
if ! command -v node >/dev/null 2>&1; then
echo "node is required but not found in PATH. Please install Node.js." >&2
exit 1
fi

if ! command -v npm >/dev/null 2>&1; then
echo "npm is required but not found in PATH. Please install Node.js and npm." >&2
exit 1
fi

# Install Azle CLI (idempotent)
echo "🔗 Installing Azle CLI..."
npm install -g azle@latest

# Install npm dependencies
echo "📦 Installing npm dependencies..."
cd ic && npm install
cd ..

# Set up dfx identity for codespace
echo "🔑 Setting up dfx identity..."
dfx identity new codespace_dev --storage-mode=plaintext || echo "Identity may already exist"
dfx identity use codespace_dev
dfx start --background
dfx stop
retry npm install -g azle@latest || echo "Warning: azle installation failed, continuing..."

# Install npm dependencies in the 'ic' subfolder if it exists
if [ -d "ic" ]; then
echo "📦 Installing npm dependencies in ic/..."
pushd ic >/dev/null
retry npm install || echo "Warning: npm install in ic failed"
popd >/dev/null
else
echo "No 'ic' directory found; skipping npm install step"
fi

# Optionally set up dfx identity if dfx is available
if command -v dfx >/dev/null 2>&1; then
echo "🔑 Setting up dfx identity..."
dfx identity new codespace_dev --storage-mode=plaintext || echo "Identity may already exist"
dfx identity use codespace_dev || true
dfx start --background || echo "Warning: dfx start failed"
dfx stop || true
else
echo "dfx not found; skipping dfx identity steps"
fi

echo "✅ Devcontainer setup complete!"
Loading