diff --git a/Crewai-agents/trip_planner/tests/test_calculator_safety.py b/Crewai-agents/trip_planner/tests/test_calculator_safety.py new file mode 100644 index 00000000..787e3462 --- /dev/null +++ b/Crewai-agents/trip_planner/tests/test_calculator_safety.py @@ -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) diff --git a/Crewai-agents/trip_planner/tools/calculator_tools.py b/Crewai-agents/trip_planner/tools/calculator_tools.py index 055097a3..aee0ef03 100644 --- a/Crewai-agents/trip_planner/tools/calculator_tools.py +++ b/Crewai-agents/trip_planner/tools/calculator_tools.py @@ -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 diff --git a/web3/internet-computer/scripts/devcontainer-setup.sh b/web3/internet-computer/scripts/devcontainer-setup.sh index 3a4d3c71..0aaacd32 100755 --- a/web3/internet-computer/scripts/devcontainer-setup.sh +++ b/web3/internet-computer/scripts/devcontainer-setup.sh @@ -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!" \ No newline at end of file