fix(core): narrow backtest subprocess environment

Closes #332

Summary:
- Narrow generated-backtest subprocess environment inheritance to OS/Python basics, proxy/certificate settings, allowed run-root config, and read-only market-data settings.
- Avoid forwarding LLM provider keys, API auth tokens, shell-tool switches, broker trading secrets, and live/advisory toggles into generated strategy code.
- Document the generated backtest trust boundary in README and SECURITY.

Validation:
- PYTHONPATH=agent pytest agent/tests/test_runner_env.py agent/tests/test_run_card.py agent/tests/test_path_safety.py agent/tests/test_file_tool_sandbox_security.py -q
- python -m ruff check agent/src/core/runner.py agent/tests/test_runner_env.py
- python -m compileall -q agent/src/core/runner.py agent/tests/test_runner_env.py
- git diff --check
- GitHub Actions CI passed
This commit is contained in:
Haozhe Wu authored and GitHub committed 2026-07-02 15:23:41 +08:00
1 parent 6ec6bd1314
commit 2cd47cd166
4 files changed
+205 -1

No files matched your search

+2
View File
@@ -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`.
+6
View File
@@ -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
+106 -1
View File
@@ -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",
+91
View File
@@ -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"