diff --git a/README.md b/README.md index 7a706d0d..75f7d084 100644 --- a/README.md +++ b/README.md @@ -830,6 +830,8 @@ For localhost development, `vibe-trading serve` keeps the browser workflow simpl Shell-capable tools are available to local CLI and trusted localhost workflows, but are not exposed to remote API sessions unless you explicitly set `VIBE_TRADING_ENABLE_SHELL_TOOLS=1`. Document and journal readers are limited to upload/import roots by default; place files under `agent/uploads`, `agent/runs`, `./uploads`, `./data`, `~/.vibe-trading/uploads`, or `~/.vibe-trading/imports`, or add a dedicated directory through `VIBE_TRADING_ALLOWED_FILE_ROOTS`. +Generated backtest code runs as a local Python subprocess and can make network requests through the configured market-data loaders. Its environment is intentionally narrow: the runner keeps OS/Python basics, proxy/certificate settings, `VIBE_TRADING_ALLOWED_RUN_ROOTS`, and read-only market-data keys such as `TUSHARE_TOKEN`, `FMP_API_KEY`, `FRED_API_KEY`, and `VIBE_TRADING_IWENCAI_KEY`. It does not pass LLM provider keys, API auth tokens, shell-tool switches, broker trading secrets, or live/advisory toggles to generated strategy code by default. + ### Web UI Settings The Web UI Settings page lets local users update the LLM provider/model, base URL, generation parameters, reasoning effort, and optional market data credentials such as the Tushare token. Settings are persisted to `agent/.env`; provider defaults are loaded from `agent/src/providers/llm_providers.json`. diff --git a/SECURITY.md b/SECURITY.md index 9aefb5e9..f28cee83 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,6 +20,12 @@ We will acknowledge your report within **5 business days** and work with you to This policy applies to the [HKUDS/Vibe-Trading](https://github.com/HKUDS/Vibe-Trading) repository. +## Generated backtest code + +Backtest runs may execute generated Python strategy code locally. Treat generated strategies as local code you review before running. The runner validates run directories and uses a narrow subprocess environment: it preserves OS/Python basics, proxy/certificate settings, allowed run-root configuration, and read-only market-data credentials needed by loaders, but it does not forward LLM provider keys, API server bearer tokens, shell-tool opt-ins, broker trading secrets, or live/advisory toggles by default. + +The backtest subprocess is still network-capable so loaders can fetch public or user-authorized market data. Do not run untrusted generated strategies in an environment that exposes sensitive files, secret-bearing proxy variables, or network services you would not trust local code to access. + ## Official channels & impersonation Vibe-Trading is an open-source finance **research** tool. We will **never** ask you to diff --git a/agent/src/core/runner.py b/agent/src/core/runner.py index dd17195a..edb8f7a7 100644 --- a/agent/src/core/runner.py +++ b/agent/src/core/runner.py @@ -16,6 +16,111 @@ from rich.console import Console console = Console(stderr=True) +_PROXY_ENV_KEYS = frozenset( + { + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + "no_proxy", + } +) + +_RUNTIME_ENV_KEYS = frozenset( + { + "PATH", + "HOME", + "USER", + "USERNAME", + "USERPROFILE", + "SHELL", + "TMPDIR", + "TEMP", + "TMP", + "SYSTEMROOT", + "WINDIR", + "COMSPEC", + "PATHEXT", + "APPDATA", + "LOCALAPPDATA", + "PROGRAMDATA", + "LANG", + "TZ", + "XDG_CACHE_HOME", + "XDG_CONFIG_HOME", + "XDG_DATA_HOME", + "VIRTUAL_ENV", + "CONDA_PREFIX", + "LD_LIBRARY_PATH", + "DYLD_LIBRARY_PATH", + "DYLD_FALLBACK_LIBRARY_PATH", + "PYTHONHOME", + "PYTHONPATH", + "PYTHONNOUSERSITE", + "REQUESTS_CA_BUNDLE", + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "CURL_CA_BUNDLE", + "TUSHARE_TOKEN", + "FINNHUB_API_KEY", + "ALPHAVANTAGE_API_KEY", + "TIINGO_API_KEY", + "FMP_API_KEY", + "FRED_API_KEY", + "VIBE_TRADING_IWENCAI_KEY", + "VIBE_TRADING_SEC_UA", + "VIBE_TRADING_DATA_CACHE", + "VIBE_TRADING_ALLOWED_RUN_ROOTS", + "CCXT_EXCHANGE", + "CCXT_TIMEOUT_MS", + "CCXT_FETCH_BUDGET_S", + "OKX_TIMEOUT_S", + "OKX_FETCH_BUDGET_S", + "RSSHUB_BASE_URL", + "RSSHUB_TIMEOUT_S", + "RSSHUB_FETCH_BUDGET_S", + "FUTU_HOST", + "FUTU_PORT", + "VIBE_TRADING_EASTMONEY_MIN_INTERVAL", + "VIBE_TRADING_SINA_MIN_INTERVAL", + "VIBE_TRADING_STOOQ_MIN_INTERVAL", + "VIBE_TRADING_YAHOO_MIN_INTERVAL", + "VIBE_TRADING_SEC_MIN_INTERVAL", + "VIBE_TRADING_FINNHUB_MIN_INTERVAL", + "VIBE_TRADING_ALPHAVANTAGE_MIN_INTERVAL", + "VIBE_TRADING_TIINGO_MIN_INTERVAL", + "VIBE_TRADING_FMP_MIN_INTERVAL", + "VIBE_TRADING_FRED_MIN_INTERVAL", + "VIBE_TRADING_IWENCAI_MIN_INTERVAL", + "VIBE_TRADING_THS_MIN_INTERVAL", + } + | _PROXY_ENV_KEYS +) + +_RUNTIME_ENV_PREFIXES = ("LC_",) + + +def _is_runtime_env_key_allowed(key: str) -> bool: + """Return whether an environment key is safe for generated backtest code.""" + + return key in _RUNTIME_ENV_KEYS or key.startswith(_RUNTIME_ENV_PREFIXES) + + +def _copy_runtime_env() -> dict[str, str]: + """Copy the narrow environment needed by the backtest subprocess. + + Generated strategy code is executed in this subprocess, so avoid inheriting + LLM, API server, broker, live-trading, or advisory credentials by default. + The allowlist keeps OS/Python basics, proxy/cert settings, and read-only + market-data configuration needed by the built-in loaders. + """ + + return {key: value for key, value in os.environ.items() if _is_runtime_env_key_allowed(key)} + + @dataclass class RunResult: """Container for runner execution outputs. @@ -178,7 +283,7 @@ class Runner: Environment mapping for subprocess. """ - env = os.environ.copy() + env = _copy_runtime_env() env.update( { "PYTHONUNBUFFERED": "1", diff --git a/agent/tests/test_runner_env.py b/agent/tests/test_runner_env.py new file mode 100644 index 00000000..ae7ca582 --- /dev/null +++ b/agent/tests/test_runner_env.py @@ -0,0 +1,91 @@ +"""Regression tests for generated backtest subprocess environment handling.""" + +from __future__ import annotations + +import os +from pathlib import Path + +from src.core.runner import Runner + + +def test_backtest_runtime_env_keeps_market_data_configuration( + monkeypatch, + tmp_path: Path, +) -> None: + allowed_values = { + "TUSHARE_TOKEN": "tushare-token", + "FINNHUB_API_KEY": "finnhub-key", + "ALPHAVANTAGE_API_KEY": "alpha-key", + "TIINGO_API_KEY": "tiingo-key", + "FMP_API_KEY": "fmp-key", + "FRED_API_KEY": "fred-key", + "VIBE_TRADING_IWENCAI_KEY": "iwencai-key", + "VIBE_TRADING_SEC_UA": "Research Bot bot@example.com", + "VIBE_TRADING_DATA_CACHE": "1", + "VIBE_TRADING_ALLOWED_RUN_ROOTS": str(tmp_path), + "VIBE_TRADING_FMP_MIN_INTERVAL": "0.5", + "CCXT_EXCHANGE": "okx", + "CCXT_TIMEOUT_MS": "12000", + "OKX_TIMEOUT_S": "20", + "OKX_FETCH_BUDGET_S": "90", + "RSSHUB_BASE_URL": "https://rss.example.test", + "RSSHUB_TIMEOUT_S": "12", + "RSSHUB_FETCH_BUDGET_S": "45", + "FUTU_HOST": "127.0.0.1", + "FUTU_PORT": "11111", + "HTTPS_PROXY": "http://proxy.example.test:8080", + "REQUESTS_CA_BUNDLE": "/tmp/ca.pem", + "LC_ALL": "C.UTF-8", + } + for key, value in allowed_values.items(): + monkeypatch.setenv(key, value) + + env = Runner(timeout=1)._build_runtime_env(tmp_path) + + for key, value in allowed_values.items(): + assert env[key] == value + assert env["PYTHONUNBUFFERED"] == "1" + assert env["PYTHONIOENCODING"] == "utf-8" + assert env["PYTHONUTF8"] == "1" + + +def test_backtest_runtime_env_scrubs_service_and_broker_secrets( + monkeypatch, + tmp_path: Path, +) -> None: + sensitive_keys = [ + "OPENAI_API_KEY", + "OPENROUTER_API_KEY", + "DEEPSEEK_API_KEY", + "LANGCHAIN_PROVIDER", + "LANGCHAIN_MODEL_NAME", + "API_AUTH_KEY", + "VIBE_TRADING_API_KEY", + "VIBE_TRADING_ENABLE_SHELL_TOOLS", + "VIBE_TRADING_ENABLE_ADVISORY", + "INVINOVERITAS_API_KEY", + "FUTU_TRADE_PWD_MD5", + "BINANCE_API_SECRET", + "ALPACA_API_KEY", + "LONGPORT_APP_SECRET", + "SHOONYA_PASSWORD", + ] + for key in sensitive_keys: + monkeypatch.setenv(key, f"{key.lower()}-secret") + + env = Runner(timeout=1)._build_runtime_env(tmp_path) + + for key in sensitive_keys: + assert key not in env + + +def test_backtest_runtime_env_prepends_runtime_pythonpath( + monkeypatch, + tmp_path: Path, +) -> None: + monkeypatch.setenv("PYTHONPATH", "existing-path") + pythonpath_extra = tmp_path / "agent" + + env = Runner(timeout=1)._build_runtime_env(tmp_path, pythonpath_extra=pythonpath_extra) + + assert env["PYTHONPATH"] == f"{pythonpath_extra}{os.pathsep}existing-path"