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:
1 parent
6ec6bd1314
commit
2cd47cd166
4 files changed
+205
-1
No files matched your search
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user