Compare commits

...

8 Commits

Author SHA1 Message Date
github-actions[bot] 13cbd42ecf chore(master): release 1.3.9 (#43)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 13:43:52 +02:00
TPTBusiness 64ed6b0cce fix(security): resolve path-injection, B701, B101, B112 Bandit alerts
- Path injection (B614): centralized safe_resolve_path in core/utils.py,
  refactored 6 UI modules to use it with safe_root validation
- B701: added explicit autoescape=select_autoescape() to Jinja2
  Environment() calls in 3 files
- B101: replaced assert statements with proper if/raise patterns in
  12+ files (partial)
- B112: added logger.warning() to bare except:continue blocks in
  5 files
2026-05-01 13:42:59 +02:00
github-actions[bot] bf36f54159 chore(master): release 1.3.8 (#42)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-30 20:01:29 +02:00
TPTBusiness 79f1d34083 fix(security): resolve path-injection and add nosec for safe temp paths (B108, py/path-injection)
- ds_trace.py: resolve() user-provided save path and use Path.name for filenames
  to prevent directory traversal in the local workspace save UI
- rl/finetune UI data_loaders: nosec B614 where paths are already validated
  against safe_root via realpath() before use
- Temp paths (/tmp/sample, /tmp/full, /tmp/mock/*, /tmp/predix_loop.pid,
  /tmp/autorl_output): nosec B108 — fixed Docker volume mount points or
  single-process admin files, not user-writable attack surface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:26:38 +02:00
TPTBusiness 150a818e07 fix(security): replace shell=True subprocess calls with list args in env.py (B602)
Converted conda commands in _update_bin_path, _sync_conda_cache_with_real_envs,
_prepare_conda_env, and FTCondaEnv.prepare() to list args. Replaced pipe-based
grep with pure Python parsing. LocalEnv.Popen retains shell=True with nosec
since entry is an internal command string set by LocalEnvConf, not user input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:26:29 +02:00
TPTBusiness b6d1caecc9 fix(security): replace eval() with ast.literal_eval in finetune validator (B307)
eval() on trainer stdout output replaced with ast.literal_eval() which only
parses Python literals and cannot execute arbitrary code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:26:23 +02:00
TPTBusiness 73e600bf25 fix(qlib): correct indentation in except blocks in quant_proposal and factor_runner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 13:30:49 +02:00
TPTBusiness 9960633d01 fix(deps): relax aiohttp constraint to >=3.13.4 for litellm compatibility
litellm 1.83.14 pins aiohttp==3.13.4 exactly; requiring >=3.13.5 caused
an unresolvable conflict in CI. aiohttp 3.13.4 still patches all four CVEs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 09:44:09 +02:00
37 changed files with 175 additions and 175 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
".": "1.3.7" ".": "1.3.9"
} }
+18
View File
@@ -1,5 +1,23 @@
# Changelog # Changelog
## [1.3.9](https://github.com/TPTBusiness/Predix/compare/v1.3.8...v1.3.9) (2026-05-01)
### Bug Fixes
* **security:** resolve path-injection, B701, B101, B112 Bandit alerts ([20b89a0](https://github.com/TPTBusiness/Predix/commit/20b89a061843b39836e975f158404e8e2d4627cd))
## [1.3.8](https://github.com/TPTBusiness/Predix/compare/v1.3.7...v1.3.8) (2026-04-30)
### Bug Fixes
* **deps:** relax aiohttp constraint to &gt;=3.13.4 for litellm compatibility ([34ab192](https://github.com/TPTBusiness/Predix/commit/34ab1923a887089eb36e5cbad6cb8df16f0333ca))
* **qlib:** correct indentation in except blocks in quant_proposal and factor_runner ([8143451](https://github.com/TPTBusiness/Predix/commit/8143451e8c0ead01c4d86d19669268c7bfb15fac))
* **security:** replace eval() with ast.literal_eval in finetune validator (B307) ([0508caf](https://github.com/TPTBusiness/Predix/commit/0508caf9140d210b823fefefa28ee535ec85a0ae))
* **security:** replace shell=True subprocess calls with list args in env.py (B602) ([2012d5a](https://github.com/TPTBusiness/Predix/commit/2012d5ae4e77cc2f1ab9a48beaaac5a74695d083))
* **security:** resolve path-injection and add nosec for safe temp paths (B108, py/path-injection) ([6727480](https://github.com/TPTBusiness/Predix/commit/67274803bd1d14e5d1df9a063f46b2edb8501a2b))
## [1.3.7](https://github.com/TPTBusiness/Predix/compare/v1.3.6...v1.3.7) (2026-04-30) ## [1.3.7](https://github.com/TPTBusiness/Predix/compare/v1.3.6...v1.3.7) (2026-04-30)
+7
View File
@@ -18,6 +18,8 @@ load_dotenv(Path(__file__).parent / ".env")
import typer import typer
from rich.console import Console from rich.console import Console
from rdagent.utils.env import logger
app = typer.Typer(help="Predix - AI Quantitative Trading Agent") app = typer.Typer(help="Predix - AI Quantitative Trading Agent")
console = Console() console = Console()
@@ -510,6 +512,7 @@ def top(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
results.append(data) results.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if not results: if not results:
@@ -659,6 +662,7 @@ def portfolio(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
results.append(data) results.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if not results: if not results:
@@ -956,6 +960,7 @@ def portfolio_simple(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
results.append(data) results.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if not results: if not results:
@@ -1337,6 +1342,7 @@ def build_strategies_ai(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
factors.append(data) factors.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if len(factors) < 10: if len(factors) < 10:
@@ -1552,6 +1558,7 @@ def _load_strategies():
try: try:
raw = json.loads(p.read_text()) raw = json.loads(p.read_text())
except Exception: except Exception:
logger.warning("Failed to load strategy file %s", p, exc_info=True)
continue continue
if not isinstance(raw, dict): if not isinstance(raw, dict):
continue continue
+4 -1
View File
@@ -27,6 +27,8 @@ import typer
from rich.console import Console from rich.console import Console
from typing_extensions import Annotated from typing_extensions import Annotated
from rdagent.utils.env import logger
from rdagent.app.data_science.loop import main as data_science from rdagent.app.data_science.loop import main as data_science
from rdagent.app.finetune.llm.loop import main as llm_finetune from rdagent.app.finetune.llm.loop import main as llm_finetune
from rdagent.app.general_model.general_model import ( from rdagent.app.general_model.general_model import (
@@ -882,6 +884,7 @@ def optimize_portfolio_cli(
if data.get("status") == "accepted": if data.get("status") == "accepted":
strategies.append(data) strategies.append(data)
except Exception: except Exception:
logger.warning("Failed to load strategy file %s", f, exc_info=True)
continue continue
if not strategies: if not strategies:
@@ -1251,7 +1254,7 @@ def start_loop_cli(
script_dir = str(Path(__file__).parent.parent.parent.parent) script_dir = str(Path(__file__).parent.parent.parent.parent)
generator = f"python {script_dir}/scripts/predix_smart_strategy_gen.py" generator = f"python {script_dir}/scripts/predix_smart_strategy_gen.py"
logfile = f"{script_dir}/results/logs/generator_loop.log" logfile = f"{script_dir}/results/logs/generator_loop.log"
pidfile = "/tmp/predix_loop.pid" pidfile = "/tmp/predix_loop.pid" # nosec B108 — administrative PID file, single-process daemon
os.makedirs(f"{script_dir}/results/logs", exist_ok=True) os.makedirs(f"{script_dir}/results/logs", exist_ok=True)
+8 -47
View File
@@ -24,46 +24,12 @@ from rdagent.app.finetune.llm.ui.ft_summary import render_job_summary
DEFAULT_LOG_BASE = "log/" DEFAULT_LOG_BASE = "log/"
from rdagent.core.utils import safe_resolve_path
def validate_path_within_cwd(user_path: Path) -> Path: def validate_path_within_cwd(user_path: Path) -> Path:
"""
Validate that a user-provided path is within the current working directory.
Security: This function prevents path traversal attacks by:
1. Resolving the path to its absolute canonical form
2. Verifying it's within the CWD boundary using a normalized common prefix
3. Rejecting paths outside the boundary with ValueError
Parameters
----------
user_path : Path
User-provided path to validate
Returns
-------
Path
Resolved absolute path if valid
Raises
------
ValueError
If path is outside the current working directory
"""
safe_root = Path.cwd().resolve() safe_root = Path.cwd().resolve()
# Expand any user home reference and resolve without requiring the path to exist. return safe_resolve_path(user_path, safe_root)
resolved_path = user_path.expanduser().resolve(strict=False)
# Ensure the resolved path is absolute and remains within the safe root.
safe_root_str = str(safe_root)
resolved_str = str(resolved_path)
common = os.path.commonpath([safe_root_str, resolved_str])
if common != safe_root_str:
raise ValueError("Path is outside the allowed project directory")
# This will raise ValueError if resolved_path is not within safe_root
resolved_path.relative_to(safe_root)
return resolved_path
def get_job_options(base_path: Path, safe_root: Path | None = None) -> list[str]: def get_job_options(base_path: Path, safe_root: Path | None = None) -> list[str]:
@@ -141,19 +107,14 @@ def main():
st.header("Job") st.header("Job")
base_folder = st.text_input("Base Folder", value=default_log, key="base_folder_input") base_folder = st.text_input("Base Folder", value=default_log, key="base_folder_input")
# Normalize and validate the base folder against the configured log root safe_root = Path(default_log).expanduser().resolve()
root_real = os.path.realpath(str(Path(default_log).expanduser())) try:
folder_real = os.path.realpath(str(Path(base_folder).expanduser())) base_path = safe_resolve_path(Path(base_folder), safe_root)
if folder_real == root_real or folder_real.startswith(root_real + os.sep): except ValueError:
base_path = Path(folder_real)
safe_root = Path(root_real)
else:
st.error("Invalid base folder: must be within the configured log directory.") st.error("Invalid base folder: must be within the configured log directory.")
safe_root = Path(root_real)
base_path = safe_root base_path = safe_root
# base_path is validated against safe_root nosec B614 job_options = get_job_options(base_path, safe_root)
job_options = get_job_options(base_path, safe_root) # nosec B614 validated above
if job_options: if job_options:
selected_job = st.selectbox("Select Job", job_options, key="job_select") selected_job = st.selectbox("Select Job", job_options, key="job_select")
if selected_job.startswith("."): if selected_job.startswith("."):
+7 -9
View File
@@ -13,6 +13,7 @@ from typing import Any
import streamlit as st import streamlit as st
from rdagent.app.finetune.llm.ui.config import EVALUATOR_CONFIG, EventType from rdagent.app.finetune.llm.ui.config import EVALUATOR_CONFIG, EventType
from rdagent.core.utils import safe_resolve_path
from rdagent.log.storage import FileStorage from rdagent.log.storage import FileStorage
@@ -89,11 +90,10 @@ def extract_stage(tag: str) -> str:
def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]: def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]:
"""Get list of valid session directories, optionally validating against a safe root.""" """Get list of valid session directories, optionally validating against a safe root."""
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
folder_real = os.path.realpath(str(log_folder.expanduser())) log_folder = safe_resolve_path(log_folder, safe_root)
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)): except ValueError:
return [] return []
log_folder = Path(folder_real)
if not log_folder.exists(): if not log_folder.exists():
return [] return []
@@ -373,13 +373,11 @@ def parse_event(tag: str, content: Any, timestamp: datetime) -> Event | None:
@st.cache_data(ttl=300, hash_funcs={Path: str}) @st.cache_data(ttl=300, hash_funcs={Path: str})
def load_ft_session(log_path: Path, safe_root: Path | None = None) -> Session: def load_ft_session(log_path: Path, safe_root: Path | None = None) -> Session:
"""Load events into hierarchical session structure, optionally validating against safe root.""" """Load events into hierarchical session structure, optionally validating against safe root."""
# Validate path is within safe_root if provided
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
path_real = os.path.realpath(str(log_path.expanduser())) log_path = safe_resolve_path(log_path, safe_root)
if not (path_real == root_real or path_real.startswith(root_real + os.sep)): except ValueError:
return Session() return Session()
log_path = Path(path_real)
session = Session() session = Session()
storage = FileStorage(log_path) storage = FileStorage(log_path)
+1
View File
@@ -322,6 +322,7 @@ class QuantRDLoop(RDLoop):
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
factors.append(data) factors.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if len(factors) < 10: if len(factors) < 10:
+5 -35
View File
@@ -16,55 +16,26 @@ from rdagent.app.rl.ui.components import render_session, render_summary
from rdagent.app.rl.ui.config import ALWAYS_VISIBLE_TYPES, OPTIONAL_TYPES from rdagent.app.rl.ui.config import ALWAYS_VISIBLE_TYPES, OPTIONAL_TYPES
from rdagent.app.rl.ui.data_loader import get_summary, get_valid_sessions, load_session from rdagent.app.rl.ui.data_loader import get_summary, get_valid_sessions, load_session
from rdagent.app.rl.ui.rl_summary import render_job_summary from rdagent.app.rl.ui.rl_summary import render_job_summary
from rdagent.core.utils import safe_resolve_path
DEFAULT_LOG_BASE = "log/" DEFAULT_LOG_BASE = "log/"
def _safe_resolve(user_input: str | None, safe_root: Path) -> Path: def _safe_resolve(user_input: str | None, safe_root: Path) -> Path:
"""
Resolve user path relative to safe_root; raise ValueError if it escapes.
Security: This function prevents path traversal attacks by:
1. Rejecting null bytes in user input
2. Rejecting Windows drive letters (C:\, D:\, etc.)
3. Rejecting absolute paths
4. Normalizing path to remove .. traversal attempts
5. Validating resolved path is within safe_root using a realpath-based check
All user-provided paths are validated before filesystem access.
"""
# Treat the provided safe_root as trusted and canonicalize it once.
safe_root = safe_root.expanduser().resolve() safe_root = safe_root.expanduser().resolve()
# Empty input maps to the safe root directory.
if not user_input: if not user_input:
return safe_root return safe_root
# Security check 1: Reject null bytes (path truncation attack)
if "\x00" in user_input: if "\x00" in user_input:
raise ValueError("Invalid path: contains null byte") raise ValueError("Invalid path: contains null byte")
try: try:
# Security check 2: Normalize path to resolve .. and . components
normalized = os.path.normpath(user_input.strip()) normalized = os.path.normpath(user_input.strip())
# Security check 3: Reject Windows drive letters (C:\, D:\, etc.)
drive, _ = os.path.splitdrive(normalized) drive, _ = os.path.splitdrive(normalized)
if drive: if drive:
raise ValueError("Absolute paths with drive letters are not allowed") raise ValueError("Absolute paths with drive letters are not allowed")
# Security check 4: Reject absolute paths (/, //server/share, etc.)
if os.path.isabs(normalized): if os.path.isabs(normalized):
raise ValueError("Absolute paths are not allowed") raise ValueError("Absolute paths are not allowed")
joined = safe_root / normalized
# Security check 5: Build candidate path under safe_root and fully resolve it. return safe_resolve_path(joined, safe_root)
joined = os.path.join(str(safe_root), normalized)
resolved_candidate = os.path.realpath(joined)
# Security check 6: Validate candidate is within safe_root (prevent path traversal)
candidate_path = Path(resolved_candidate)
# Reconstruct from trusted safe_root so the returned path is root-derived.
return safe_root / candidate_path.relative_to(safe_root)
except (OSError, ValueError) as exc: except (OSError, ValueError) as exc:
raise ValueError(f"Invalid path outside of allowed root: {user_input}") from exc raise ValueError(f"Invalid path outside of allowed root: {user_input}") from exc
@@ -82,7 +53,7 @@ def get_job_options(base_path: Path, safe_root: Path | None = None) -> list[str]
# Security fix: Validate base_path to prevent path traversal # Security fix: Validate base_path to prevent path traversal
try: try:
base_path_resolved = base_path.expanduser().resolve() base_path_resolved = base_path.expanduser().resolve() # nosec B614 — validated against safe_root below via relative_to()
if safe_root is not None: if safe_root is not None:
safe_root_resolved = safe_root.expanduser().resolve() safe_root_resolved = safe_root.expanduser().resolve()
@@ -203,8 +174,7 @@ def main():
except ValueError as e: except ValueError as e:
st.warning(str(e)) st.warning(str(e))
return return
# job_path is validated by _safe_resolve() above if job_path.exists():
if job_path.exists(): # nosec B614 path validated by _safe_resolve
render_job_summary(job_path, safe_root, is_root=is_root_job) render_job_summary(job_path, safe_root, is_root=is_root_job)
else: else:
st.warning(f"Job folder not found: {job_folder}") st.warning(f"Job folder not found: {job_folder}")
+7 -9
View File
@@ -15,6 +15,7 @@ from typing import Any
import streamlit as st import streamlit as st
from rdagent.app.rl.ui.config import EventType from rdagent.app.rl.ui.config import EventType
from rdagent.core.utils import safe_resolve_path
from rdagent.log.storage import FileStorage from rdagent.log.storage import FileStorage
@@ -76,11 +77,10 @@ def extract_stage(tag: str) -> str:
def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]: def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]:
"""Get list of valid session directories, optionally validating against a safe root.""" """Get list of valid session directories, optionally validating against a safe root."""
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
folder_real = os.path.realpath(str(log_folder.expanduser())) log_folder = safe_resolve_path(log_folder, safe_root)
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)): except ValueError:
return [] return []
log_folder = Path(folder_real)
if not log_folder.exists(): if not log_folder.exists():
return [] return []
@@ -245,13 +245,11 @@ def parse_event(tag: str, content: Any, timestamp: datetime) -> Event | None:
@st.cache_data(ttl=300, hash_funcs={Path: str}) @st.cache_data(ttl=300, hash_funcs={Path: str})
def load_session(log_path: Path, safe_root: Path | None = None) -> Session: def load_session(log_path: Path, safe_root: Path | None = None) -> Session:
"""Load events into hierarchical session structure, optionally validating against safe root.""" """Load events into hierarchical session structure, optionally validating against safe root."""
# Validate path is within safe_root if provided
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
path_real = os.path.realpath(str(log_path.expanduser())) log_path = safe_resolve_path(log_path, safe_root)
if not (path_real == root_real or path_real.startswith(root_real + os.sep)): except ValueError:
return Session() return Session()
log_path = Path(path_real)
session = Session() session = Session()
+4 -6
View File
@@ -9,6 +9,8 @@ from pathlib import Path
import pandas as pd import pandas as pd
import streamlit as st import streamlit as st
from rdagent.core.utils import safe_resolve_path
def is_valid_task(task_path: Path) -> bool: def is_valid_task(task_path: Path) -> bool:
"""Check if directory is a valid RL task (has __session__ subdirectory)""" """Check if directory is a valid RL task (has __session__ subdirectory)"""
@@ -62,14 +64,10 @@ def get_loop_status(task_path: Path, loop_id: int) -> tuple[str, bool | None]:
def _validate_job_path(job_path: Path, safe_root: Path) -> Path: def _validate_job_path(job_path: Path, safe_root: Path) -> Path:
"""Resolve and validate that job_path stays within safe_root."""
resolved_root = safe_root.expanduser().resolve()
resolved_job = job_path.expanduser().resolve()
try: try:
# Reconstruct from trusted root so the returned path is root-derived. return safe_resolve_path(job_path, safe_root)
return resolved_root / resolved_job.relative_to(resolved_root)
except ValueError: except ValueError:
raise ValueError(f"Job path is outside allowed root {resolved_root}") raise ValueError(f"Job path is outside allowed root {safe_root}")
def get_max_loops(job_path: Path, safe_root: Path | None = None) -> int: def get_max_loops(job_path: Path, safe_root: Path | None = None) -> int:
@@ -54,7 +54,8 @@ def get_ds_env(
ValueError: If the env_type is not recognized. ValueError: If the env_type is not recognized.
""" """
conf = DSCoderCoSTEERSettings() conf = DSCoderCoSTEERSettings()
assert conf_type in ["kaggle", "mlebench"], f"Unknown conf_type: {conf_type}" if conf_type not in ["kaggle", "mlebench"]:
raise ValueError(f"Unknown conf_type: {conf_type}")
if conf.env_type == "docker": if conf.env_type == "docker":
env_conf = DSDockerConf() if conf_type == "kaggle" else MLEBDockerConf() env_conf = DSDockerConf() if conf_type == "kaggle" else MLEBDockerConf()
@@ -79,7 +80,8 @@ def get_clear_ws_cmd(stage: Literal["before_training", "before_inference"] = "be
""" """
Clean the files in workspace to a specific stage Clean the files in workspace to a specific stage
""" """
assert stage in ["before_training", "before_inference"], f"Unknown stage: {stage}" if stage not in ["before_training", "before_inference"]:
raise ValueError(f"Unknown stage: {stage}")
if DS_RD_SETTING.enable_model_dump and stage == "before_training": if DS_RD_SETTING.enable_model_dump and stage == "before_training":
cmd = "rm -r submission.csv scores.csv models trace.log" cmd = "rm -r submission.csv scores.csv models trace.log"
else: else:
@@ -13,7 +13,7 @@ File structure
from pathlib import Path from pathlib import Path
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined, select_autoescape
from rdagent.app.data_science.conf import DS_RD_SETTING from rdagent.app.data_science.conf import DS_RD_SETTING
from rdagent.components.coder.CoSTEER.evaluators import ( from rdagent.components.coder.CoSTEER.evaluators import (
@@ -88,7 +88,7 @@ class EnsembleMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
code_spec = workspace.file_dict["spec/ensemble.md"] code_spec = workspace.file_dict["spec/ensemble.md"]
else: else:
test_code = ( test_code = (
Environment(undefined=StrictUndefined) Environment(undefined=StrictUndefined, autoescape=select_autoescape())
.from_string((DIRNAME / "eval_tests" / "ensemble_test.txt").read_text()) .from_string((DIRNAME / "eval_tests" / "ensemble_test.txt").read_text())
.render( .render(
model_names=[ model_names=[
@@ -2,7 +2,7 @@ import json
import re import re
from pathlib import Path from pathlib import Path
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined, select_autoescape
from rdagent.app.data_science.conf import DS_RD_SETTING from rdagent.app.data_science.conf import DS_RD_SETTING
from rdagent.components.coder.CoSTEER.evaluators import ( from rdagent.components.coder.CoSTEER.evaluators import (
@@ -55,7 +55,7 @@ class EnsembleCoSTEEREvaluator(CoSTEEREvaluator):
fname = "test/ensemble_test.txt" fname = "test/ensemble_test.txt"
test_code = (DIRNAME / "eval_tests" / "ensemble_test.txt").read_text() test_code = (DIRNAME / "eval_tests" / "ensemble_test.txt").read_text()
test_code = ( test_code = (
Environment(undefined=StrictUndefined) Environment(undefined=StrictUndefined, autoescape=select_autoescape())
.from_string(test_code) .from_string(test_code)
.render( .render(
model_names=[ model_names=[
@@ -6,6 +6,7 @@ Two-step validation:
2. Micro-batch testing - Runtime validation with small dataset 2. Micro-batch testing - Runtime validation with small dataset
""" """
import ast
import json import json
import re import re
import time import time
@@ -229,7 +230,7 @@ class LLMConfigValidator:
final_metrics = re.search(r"\{'train_runtime':[^}]+\}", stdout) final_metrics = re.search(r"\{'train_runtime':[^}]+\}", stdout)
if final_metrics: if final_metrics:
try: try:
metrics = eval(final_metrics.group(0)) # Safe: only numbers and strings metrics = ast.literal_eval(final_metrics.group(0))
result["final_metrics"] = { result["final_metrics"] = {
"train_loss": metrics.get("train_loss"), "train_loss": metrics.get("train_loss"),
"train_runtime": metrics.get("train_runtime"), "train_runtime": metrics.get("train_runtime"),
@@ -41,7 +41,8 @@ class ModelCoSTEEREvaluator(CoSTEEREvaluator):
final_feedback="This task has failed too many times, skip implementation.", final_feedback="This task has failed too many times, skip implementation.",
final_decision=False, final_decision=False,
) )
assert isinstance(target_task, ModelTask) if not isinstance(target_task, ModelTask):
raise TypeError(f"Expected ModelTask, got {type(target_task)}")
# NOTE: Use fixed input to test the model to avoid randomness # NOTE: Use fixed input to test the model to avoid randomness
batch_size = 8 batch_size = 8
@@ -50,7 +51,8 @@ class ModelCoSTEEREvaluator(CoSTEEREvaluator):
input_value = 0.4 input_value = 0.4
param_init_value = 0.6 param_init_value = 0.6
assert isinstance(implementation, ModelFBWorkspace) if not isinstance(implementation, ModelFBWorkspace):
raise TypeError(f"Expected ModelFBWorkspace, got {type(implementation)}")
model_execution_feedback, gen_np_array = implementation.execute( model_execution_feedback, gen_np_array = implementation.execute(
batch_size=batch_size, batch_size=batch_size,
num_features=num_features, num_features=num_features,
@@ -59,7 +61,8 @@ class ModelCoSTEEREvaluator(CoSTEEREvaluator):
param_init_value=param_init_value, param_init_value=param_init_value,
) )
if gt_implementation is not None: if gt_implementation is not None:
assert isinstance(gt_implementation, ModelFBWorkspace) if not isinstance(gt_implementation, ModelFBWorkspace):
raise TypeError(f"Expected ModelFBWorkspace, got {type(gt_implementation)}")
_, gt_np_array = gt_implementation.execute( _, gt_np_array = gt_implementation.execute(
batch_size=batch_size, batch_size=batch_size,
num_features=num_features, num_features=num_features,
@@ -24,7 +24,8 @@ class UndirectedNode(Node):
super().__init__(content, label, embedding) super().__init__(content, label, embedding)
self.neighbors: set[UndirectedNode] = set() self.neighbors: set[UndirectedNode] = set()
self.appendix = appendix # appendix stores any additional information self.appendix = appendix # appendix stores any additional information
assert isinstance(content, str), "content must be a string" if not isinstance(content, str):
raise TypeError("content must be a string")
def add_neighbor(self, node: UndirectedNode) -> None: def add_neighbor(self, node: UndirectedNode) -> None:
self.neighbors.add(node) self.neighbors.add(node)
@@ -96,7 +97,8 @@ class Graph(KnowledgeBase):
APIBackend().create_embedding(input_content=contents[i : i + size]), APIBackend().create_embedding(input_content=contents[i : i + size]),
) )
assert len(nodes) == len(embeddings), "nodes' length must equals embeddings' length" if len(nodes) != len(embeddings):
raise ValueError("nodes' length must equal embeddings' length")
for node, embedding in zip(nodes, embeddings): for node, embedding in zip(nodes, embeddings):
node.embedding = embedding node.embedding = embedding
return nodes return nodes
@@ -252,7 +254,8 @@ class UndirectedGraph(Graph):
""" """
min_nodes_count = 2 min_nodes_count = 2
assert len(nodes) >= min_nodes_count, "nodes length must >=2" if len(nodes) < min_nodes_count:
raise ValueError("nodes length must >=2")
intersection = None intersection = None
for node in nodes: for node in nodes:
+11
View File
@@ -4,6 +4,7 @@ import functools
import importlib import importlib
import json import json
import multiprocessing as mp import multiprocessing as mp
import os
import pickle import pickle
import random import random
from collections.abc import Callable from collections.abc import Callable
@@ -208,3 +209,13 @@ def cache_with_pickle(hash_func: Callable, post_process_func: Callable | None =
return cache_wrapper return cache_wrapper
return cache_decorator return cache_decorator
def safe_resolve_path(user_path: Path, safe_root: Path | None = None) -> Path:
if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser()))
path_real = os.path.realpath(str(user_path.expanduser())) # nosec B614 — validated against safe_root below
if not (path_real == root_real or path_real.startswith(root_real + os.sep)):
raise ValueError(f"Path {user_path} resolves to {path_real}, outside allowed root {safe_root}")
return Path(path_real)
return user_path.expanduser().resolve()
+10 -3
View File
@@ -30,6 +30,7 @@ from rdagent.log.utils import (
extract_loopid_func_name, extract_loopid_func_name,
is_valid_session, is_valid_session,
) )
from rdagent.core.utils import safe_resolve_path
from rdagent.oai.backend.litellm import LITELLM_SETTINGS from rdagent.oai.backend.litellm import LITELLM_SETTINGS
from rdagent.oai.llm_utils import APIBackend from rdagent.oai.llm_utils import APIBackend
@@ -232,13 +233,19 @@ def workspace_win(workspace, cmp_workspace=None, cmp_name="last code."):
if target_folder.strip() == "": if target_folder.strip() == "":
st.warning("Please enter a valid folder path.") st.warning("Please enter a valid folder path.")
else: else:
target_folder_path = Path(target_folder) safe_root = Path(UI_SETTING.trace_folder).expanduser().resolve()
safe_root.mkdir(parents=True, exist_ok=True)
try:
target_folder_path = safe_resolve_path(Path(target_folder), safe_root)
except ValueError:
st.warning(f"Path must be within {safe_root}. Saving to default location.")
target_folder_path = safe_root
target_folder_path.mkdir(parents=True, exist_ok=True) target_folder_path.mkdir(parents=True, exist_ok=True)
for filename, content in workspace.file_dict.items(): for filename, content in workspace.file_dict.items():
save_path = target_folder_path / filename save_path = target_folder_path / Path(filename).name
save_path.parent.mkdir(parents=True, exist_ok=True) save_path.parent.mkdir(parents=True, exist_ok=True)
save_path.write_text(content, encoding="utf-8") save_path.write_text(content, encoding="utf-8")
st.success(f"All files saved to: {target_folder}") st.success(f"All files saved to: {target_folder_path}")
else: else:
st.markdown(f"No files in :blue[{replace_ep_path(workspace.workspace_path)}]") st.markdown(f"No files in :blue[{replace_ep_path(workspace.workspace_path)}]")
+6 -4
View File
@@ -36,16 +36,18 @@ def get_agent_model() -> OpenAIChatModel:
""" """
backend = APIBackend() backend = APIBackend()
assert isinstance(backend, LiteLLMAPIBackend), "Only LiteLLMAPIBackend is supported" if not isinstance(backend, LiteLLMAPIBackend):
raise TypeError("Only LiteLLMAPIBackend is supported")
compl_kwargs = backend.get_complete_kwargs() compl_kwargs = backend.get_complete_kwargs()
selected_model = compl_kwargs["model"] selected_model = compl_kwargs["model"]
_, custom_llm_provider, _, _ = get_llm_provider(selected_model) _, custom_llm_provider, _, _ = get_llm_provider(selected_model)
assert ( if custom_llm_provider not in PROVIDER_TO_ENV_MAP:
custom_llm_provider in PROVIDER_TO_ENV_MAP raise ValueError(
), f"Provider {custom_llm_provider} not supported. Please add it into `PROVIDER_TO_ENV_MAP`" f"Provider {custom_llm_provider} not supported. Please add it into `PROVIDER_TO_ENV_MAP`"
)
prefix = PROVIDER_TO_ENV_MAP[custom_llm_provider] prefix = PROVIDER_TO_ENV_MAP[custom_llm_provider]
api_key = os.getenv(f"{prefix}_API_KEY", None) api_key = os.getenv(f"{prefix}_API_KEY", None)
api_base = os.getenv(f"{prefix}_API_BASE", None) api_base = os.getenv(f"{prefix}_API_BASE", None)
@@ -7,8 +7,10 @@ from sklearn.metrics import roc_auc_score
def prepare_for_auroc_metric(submission: pd.DataFrame, answers: pd.DataFrame, id_col: str, target_col: str) -> dict: def prepare_for_auroc_metric(submission: pd.DataFrame, answers: pd.DataFrame, id_col: str, target_col: str) -> dict:
# Answers checks # Answers checks
assert id_col in answers.columns, f"answers dataframe should have an {id_col} column" if id_col not in answers.columns:
assert target_col in answers.columns, f"answers dataframe should have a {target_col} column" raise InvalidSubmissionError(f"answers dataframe should have an {id_col} column")
if target_col not in answers.columns:
raise InvalidSubmissionError(f"answers dataframe should have a {target_col} column")
# Submission checks # Submission checks
if id_col not in submission.columns: if id_col not in submission.columns:
@@ -1,7 +1,8 @@
from pathlib import Path from pathlib import Path
# Check if our submission file exists # Check if our submission file exists
assert Path("submission.csv").exists(), "Error: submission.csv not found" if not Path("submission.csv").exists():
raise FileNotFoundError("Error: submission.csv not found")
submission_lines = Path("submission.csv").read_text().splitlines() submission_lines = Path("submission.csv").read_text().splitlines()
test_lines = Path("submission_test.csv").read_text().splitlines() test_lines = Path("submission_test.csv").read_text().splitlines()
@@ -22,7 +22,8 @@ def prepare_for_metric(submission: pd.DataFrame, answers: pd.DataFrame) -> dict:
if "price" not in submission.columns: if "price" not in submission.columns:
raise InvalidSubmissionError("Submission DataFrame must contain 'price' columns.") raise InvalidSubmissionError("Submission DataFrame must contain 'price' columns.")
assert "price" in answers.columns, "Answers DataFrame must contain 'price' columns." if "price" not in answers.columns:
raise InvalidSubmissionError("Answers DataFrame must contain 'price' columns.")
if len(submission) != len(answers): if len(submission) != len(answers):
raise InvalidSubmissionError("Submission must be the same length as the answers.") raise InvalidSubmissionError("Submission must be the same length as the answers.")
@@ -1,7 +1,8 @@
from pathlib import Path from pathlib import Path
# Check if our submission file exists # Check if our submission file exists
assert Path("submission.csv").exists(), "Error: submission.csv not found" if not Path("submission.csv").exists():
raise FileNotFoundError("Error: submission.csv not found")
submission_lines = Path("submission.csv").read_text().splitlines() # 自动生成的 submission_lines = Path("submission.csv").read_text().splitlines() # 自动生成的
test_lines = Path("submission_test.csv").read_text().splitlines() # test.csv test_lines = Path("submission_test.csv").read_text().splitlines() # test.csv
@@ -25,11 +25,12 @@ def prepare(raw: Path, public: Path, private: Path):
new_test.to_csv(public / "test.csv", index=False) new_test.to_csv(public / "test.csv", index=False)
# Checks # Checks
assert new_test.shape[1] == 12, "Public test set should have 12 columns" if new_test.shape[1] != 12:
assert new_train.shape[1] == 13, "Public train set should have 13 columns" raise AssertionError("Public test set should have 12 columns")
assert len(new_train) + len(new_test) == len( if new_train.shape[1] != 13:
old_train raise AssertionError("Public train set should have 13 columns")
), "Length of new_train and new_test should equal length of old_train" if len(new_train) + len(new_test) != len(old_train):
raise AssertionError("Length of new_train and new_test should equal length of old_train")
if __name__ == "__main__": if __name__ == "__main__":
@@ -293,7 +293,7 @@ class ValidationSelector(SOTAexpSelector):
Sorts all valid experiments by score and returns the top N. Sorts all valid experiments by score and returns the top N.
""" """
mock_folder = f"/tmp/mock/{self.competition}" mock_folder = f"/tmp/mock/{self.competition}" # nosec B108 — Docker volume mount point derived from internal competition name
try: try:
data_py_code, grade_py_code = self._prepare_validation_scripts( data_py_code, grade_py_code = self._prepare_validation_scripts(
@@ -540,7 +540,7 @@ def process_experiment(
# Run main script # Run main script
env = get_ds_env( env = get_ds_env(
extra_volumes={f"/tmp/mock/{competition}/{input_folder}": input_folder}, extra_volumes={f"/tmp/mock/{competition}/{input_folder}": input_folder}, # nosec B108 — Docker volume mount point derived from internal competition name
running_timeout_period=DS_RD_SETTING.full_timeout, running_timeout_period=DS_RD_SETTING.full_timeout,
) )
result = ws.run(env=env, entry="python main.py") result = ws.run(env=env, entry="python main.py")
+2 -1
View File
@@ -35,7 +35,8 @@ def select(X: pd.DataFrame) -> pd.DataFrame:
class KGModelFeatureSelectionCoder(Developer[KGModelExperiment]): class KGModelFeatureSelectionCoder(Developer[KGModelExperiment]):
def develop(self, exp: KGModelExperiment) -> KGModelExperiment: def develop(self, exp: KGModelExperiment) -> KGModelExperiment:
target_model_type = exp.sub_tasks[0].model_type target_model_type = exp.sub_tasks[0].model_type
assert target_model_type in KG_SELECT_MAPPING if target_model_type not in KG_SELECT_MAPPING:
raise ValueError(f"target_model_type {target_model_type} not in KG_SELECT_MAPPING")
if len(exp.experiment_workspace.data_description) == 1: if len(exp.experiment_workspace.data_description) == 1:
code = ( code = (
Environment(undefined=StrictUndefined) # nosec B701 — renders Python code templates, not HTML; autoescape would corrupt code Environment(undefined=StrictUndefined) # nosec B701 — renders Python code templates, not HTML; autoescape would corrupt code
@@ -1002,7 +1002,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]):
series.to_frame().to_parquet(str(parquet_path)) series.to_frame().to_parquet(str(parquet_path))
except Exception: except Exception:
logging.debug("Error in save_factor_values_to_parquet", exc_info=True) logging.debug("Error in save_factor_values_to_parquet", exc_info=True)
def _log_result_warnings(self, factor_name: str, result, metrics: dict) -> None: def _log_result_warnings(self, factor_name: str, result, metrics: dict) -> None:
""" """
@@ -241,6 +241,7 @@ class StrategyBuilder:
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
factors.append(data) factors.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
# Sort by absolute IC # Sort by absolute IC
+2 -1
View File
@@ -30,7 +30,8 @@ def _build_execute_calls(exp: QlibFactorExperiment, base_feature_workspaces: lis
execute_calls = [] execute_calls = []
if exp.sub_tasks: if exp.sub_tasks:
assert isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback) if not isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback):
raise TypeError("exp.prop_dev_feedback must be of type CoSTEERMultiFeedback")
execute_calls.extend( execute_calls.extend(
(implementation.execute, ("All",)) (implementation.execute, ("All",))
for implementation, feedback in zip(exp.sub_workspace_list, exp.prop_dev_feedback) for implementation, feedback in zip(exp.sub_workspace_list, exp.prop_dev_feedback)
+12 -10
View File
@@ -4,7 +4,7 @@ import shutil
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined, select_autoescape
from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS
from rdagent.utils.env import QTDockerEnv from rdagent.utils.env import QTDockerEnv
@@ -21,14 +21,16 @@ def generate_data_folder_from_qlib():
entry=f"python generate.py", entry=f"python generate.py",
) )
assert (Path(__file__).parent / "factor_data_template" / "intraday_pv_all.h5").exists(), ( if not (Path(__file__).parent / "factor_data_template" / "intraday_pv_all.h5").exists():
"intraday_pv_all.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n" raise FileNotFoundError(
+ execute_log "intraday_pv_all.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n"
) + execute_log
assert (Path(__file__).parent / "factor_data_template" / "intraday_pv_debug.h5").exists(), ( )
"intraday_pv_debug.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n" if not (Path(__file__).parent / "factor_data_template" / "intraday_pv_debug.h5").exists():
+ execute_log raise FileNotFoundError(
) "intraday_pv_debug.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n"
+ execute_log
)
Path(FACTOR_COSTEER_SETTINGS.data_folder).mkdir(parents=True, exist_ok=True) Path(FACTOR_COSTEER_SETTINGS.data_folder).mkdir(parents=True, exist_ok=True)
shutil.copy( shutil.copy(
@@ -67,7 +69,7 @@ def get_file_desc(p: Path, variable_list=[]) -> str:
""" """
p = Path(p) p = Path(p)
JJ_TPL = Environment(undefined=StrictUndefined).from_string(""" # nosec B701 — renders plain text description, not HTML; autoescape not applicable JJ_TPL = Environment(undefined=StrictUndefined, autoescape=select_autoescape()).from_string("""
# {{file_name}} # {{file_name}}
## File Type ## File Type
@@ -176,7 +176,7 @@ class QlibQuantHypothesisGen(FactorAndModelHypothesisGen):
ic_val = exp.result.loc["IC"] if "IC" in exp.result.index else "" ic_val = exp.result.loc["IC"] if "IC" in exp.result.index else ""
ic_str = f" IC={ic_val:.4f}" if ic_val != "" else "" ic_str = f" IC={ic_val:.4f}" if ic_val != "" else ""
except Exception: except Exception:
logging.debug("Error getting IC", exc_info=True) logging.debug("Error getting IC", exc_info=True)
decision_str = "PASS" if fb.decision else "FAIL" decision_str = "PASS" if fb.decision else "FAIL"
obs_short = (fb.observations or "")[:120].replace("\n", " ") obs_short = (fb.observations or "")[:120].replace("\n", " ")
summary_lines.append(f"- [{decision_str}]{ic_str} {names_str}: {obs_short}") summary_lines.append(f"- [{decision_str}]{ic_str} {names_str}: {obs_short}")
@@ -77,6 +77,7 @@ def count_valid_factors() -> int:
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
count += 1 count += 1
except Exception: except Exception:
logger.warning("Failed to load factor file %s", json_file, exc_info=True)
continue continue
return count return count
@@ -71,7 +71,7 @@ def submit_for_grading(grading_url: str, model_path: str) -> dict | None:
def main(): def main():
MODEL_PATH = os.environ.get("MODEL_PATH") MODEL_PATH = os.environ.get("MODEL_PATH")
DATA_PATH = os.environ.get("DATA_PATH") DATA_PATH = os.environ.get("DATA_PATH")
OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/autorl_output") OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/autorl_output") # nosec B108 — Docker container output dir, configurable via env var
GRADING_SERVER_URL = os.environ.get("GRADING_SERVER_URL", "") GRADING_SERVER_URL = os.environ.get("GRADING_SERVER_URL", "")
TRAIN_RATIO = float(os.environ.get("TRAIN_RATIO", "0.05")) TRAIN_RATIO = float(os.environ.get("TRAIN_RATIO", "0.05"))
NUM_EPOCHS = int(os.environ.get("NUM_EPOCHS", "3")) NUM_EPOCHS = int(os.environ.get("NUM_EPOCHS", "3"))
+4 -3
View File
@@ -9,7 +9,7 @@ from pathlib import Path
from typing import Any from typing import Any
import yaml import yaml
from jinja2 import Environment, FunctionLoader, StrictUndefined from jinja2 import Environment, FunctionLoader, StrictUndefined, select_autoescape
from rdagent.core.conf import RD_AGENT_SETTINGS from rdagent.core.conf import RD_AGENT_SETTINGS
from rdagent.log import rdagent_logger as logger from rdagent.log import rdagent_logger as logger
@@ -38,7 +38,8 @@ def load_content(uri: str, caller_dir: Path | None = None, ftype: str = "yaml")
caller_dir = get_caller_dir(upshift=1) caller_dir = get_caller_dir(upshift=1)
# Parse the URI # Parse the URI
path_part, *yaml_trace = uri.split(":") path_part, *yaml_trace = uri.split(":")
assert len(yaml_trace) <= 1, f"Invalid uri {uri}, only one yaml trace is allowed." if len(yaml_trace) > 1:
raise ValueError(f"Invalid uri {uri}, only one yaml trace is allowed.")
yaml_trace = [key for yt in yaml_trace for key in yt.split(".")] yaml_trace = [key for yt in yaml_trace for key in yt.split(".")]
# load file_path with priorities. # load file_path with priorities.
@@ -126,7 +127,7 @@ class RDAT:
# loader=FunctionLoader(load_conent) is for supporting grammar like below. # loader=FunctionLoader(load_conent) is for supporting grammar like below.
# `{% include "scenarios.data_science.share:component_spec.DataLoadSpec" %}` # `{% include "scenarios.data_science.share:component_spec.DataLoadSpec" %}`
rendered = ( rendered = (
Environment(undefined=StrictUndefined, loader=FunctionLoader(load_content)) Environment(undefined=StrictUndefined, loader=FunctionLoader(load_content), autoescape=select_autoescape())
.from_string(self.template) .from_string(self.template)
.render(**context) .render(**context)
.strip("\n") .strip("\n")
+18 -15
View File
@@ -614,7 +614,7 @@ class LocalEnv(Env[ASpecificLocalConf]):
if self.conf.extra_volumes is not None: if self.conf.extra_volumes is not None:
for lp, rp in self.conf.extra_volumes.items(): for lp, rp in self.conf.extra_volumes.items():
volumes[lp] = rp["bind"] if isinstance(rp, dict) else rp volumes[lp] = rp["bind"] if isinstance(rp, dict) else rp
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
Path(cache_path).mkdir(parents=True, exist_ok=True) Path(cache_path).mkdir(parents=True, exist_ok=True)
volumes[cache_path] = T("scenarios.data_science.share:scen.cache_path").r() volumes[cache_path] = T("scenarios.data_science.share:scen.cache_path").r()
for lp, rp in running_extra_volume.items(): for lp, rp in running_extra_volume.items():
@@ -678,7 +678,7 @@ class LocalEnv(Env[ASpecificLocalConf]):
cwd = Path(local_path).resolve() if local_path else None cwd = Path(local_path).resolve() if local_path else None
env = {k: str(v) if isinstance(v, int) else v for k, v in env.items()} env = {k: str(v) if isinstance(v, int) else v for k, v in env.items()}
process = subprocess.Popen( process = subprocess.Popen( # nosec B602 — entry is an internal command string set by LocalEnvConf, not user input
entry, entry,
cwd=cwd, cwd=cwd,
env={**os.environ, **env}, env={**os.environ, **env},
@@ -761,12 +761,15 @@ class CondaConf(LocalConf):
to ensure bin_path is set correctly even if the conda env was just created. to ensure bin_path is set correctly even if the conda env was just created.
""" """
conda_path_result = subprocess.run( conda_path_result = subprocess.run(
f"conda run -n {self.conda_env_name} --no-capture-output env | grep '^PATH='", ["conda", "run", "-n", self.conda_env_name, "--no-capture-output", "env"],
capture_output=True, capture_output=True,
text=True, text=True,
shell=True,
) )
self.bin_path = conda_path_result.stdout.strip().split("=")[1] if conda_path_result.returncode == 0 else "" if conda_path_result.returncode == 0:
path_lines = [l for l in conda_path_result.stdout.splitlines() if l.startswith("PATH=")]
self.bin_path = path_lines[0].split("=", 1)[1] if path_lines else ""
else:
self.bin_path = ""
class MLECondaConf(CondaConf): class MLECondaConf(CondaConf):
@@ -886,10 +889,9 @@ def _sync_conda_cache_with_real_envs() -> None:
"""Ensure the prepared cache includes environments that already exist on disk.""" """Ensure the prepared cache includes environments that already exist on disk."""
try: try:
result = subprocess.run( result = subprocess.run(
"conda env list", ["conda", "env", "list"],
capture_output=True, capture_output=True,
text=True, text=True,
shell=True,
check=False, check=False,
) )
except Exception as exc: # pragma: no cover - best-effort helper except Exception as exc: # pragma: no cover - best-effort helper
@@ -922,14 +924,15 @@ def _prepare_conda_env(env_name: str, requirements_file: Path, python_version: s
python_version: Python version for the environment python_version: Python version for the environment
""" """
# 1. Create conda environment if not exists # 1. Create conda environment if not exists
result = subprocess.run(f"conda env list | grep -q '^{env_name} '", shell=True) env_list = subprocess.run(["conda", "env", "list"], capture_output=True, text=True, check=False)
if result.returncode != 0: env_exists = any(line.split()[0] == env_name for line in env_list.stdout.splitlines() if line and not line.startswith("#"))
if not env_exists:
print(f"[yellow]Creating conda env '{env_name}' (Python {python_version})...[/yellow]") print(f"[yellow]Creating conda env '{env_name}' (Python {python_version})...[/yellow]")
subprocess.check_call(f"conda create -y -n {env_name} python={python_version}", shell=True) subprocess.check_call(["conda", "create", "-y", "-n", env_name, f"python={python_version}"])
subprocess.check_call(f"conda run -n {env_name} pip install --upgrade pip", shell=True) subprocess.check_call(["conda", "run", "-n", env_name, "pip", "install", "--upgrade", "pip"])
print(f"[yellow]Installing dependencies from {requirements_file.name}...[/yellow]") print(f"[yellow]Installing dependencies from {requirements_file.name}...[/yellow]")
subprocess.check_call(f"conda run -n {env_name} pip install -r {requirements_file}", shell=True) subprocess.check_call(["conda", "run", "-n", env_name, "pip", "install", "-r", str(requirements_file)])
print(f"[green]Conda env '{env_name}' ready[/green]") print(f"[green]Conda env '{env_name}' ready[/green]")
_CONDA_ENV_PREPARED.add(env_name) _CONDA_ENV_PREPARED.add(env_name)
@@ -969,8 +972,8 @@ class FTCondaEnv(LocalEnv[FTCondaConf]):
# Note: flash-attn>=2.8 is required for B200 (sm_100) support # Note: flash-attn>=2.8 is required for B200 (sm_100) support
print("[yellow]Installing flash-attn (compiling, may take a few minutes)...[/yellow]") print("[yellow]Installing flash-attn (compiling, may take a few minutes)...[/yellow]")
subprocess.check_call( subprocess.check_call(
f"conda run -n {self.conf.conda_env_name} pip install 'flash-attn>=2.8' --no-build-isolation --no-cache-dir", ["conda", "run", "-n", self.conf.conda_env_name, "pip", "install",
shell=True, "flash-attn>=2.8", "--no-build-isolation", "--no-cache-dir"],
) )
# Re-update bin_path after prepare() in case the conda env was just created # Re-update bin_path after prepare() in case the conda env was just created
@@ -1440,7 +1443,7 @@ class DockerEnv(Env[DockerConf]):
if self.conf.extra_volumes is not None: if self.conf.extra_volumes is not None:
for lp, rp in self.conf.extra_volumes.items(): for lp, rp in self.conf.extra_volumes.items():
volumes[lp] = rp if isinstance(rp, dict) else {"bind": rp, "mode": self.conf.extra_volume_mode} volumes[lp] = rp if isinstance(rp, dict) else {"bind": rp, "mode": self.conf.extra_volume_mode}
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
Path(cache_path).mkdir(parents=True, exist_ok=True) Path(cache_path).mkdir(parents=True, exist_ok=True)
volumes[cache_path] = { volumes[cache_path] = {
"bind": T("scenarios.data_science.share:scen.cache_path").r(), "bind": T("scenarios.data_science.share:scen.cache_path").r(),
+2 -1
View File
@@ -30,7 +30,8 @@ def wait_retry(
>>> counter >>> counter
2 2
""" """
assert retry_n > 0, "retry_n should be greater than 0" if retry_n <= 0:
raise ValueError("retry_n should be greater than 0")
def decorator(f: Callable[..., ASpecificRet]) -> Callable[..., ASpecificRet]: def decorator(f: Callable[..., ASpecificRet]) -> Callable[..., ASpecificRet]:
def wrapper(*args: Any, **kwargs: Any) -> ASpecificRet: def wrapper(*args: Any, **kwargs: Any) -> ASpecificRet:
+1 -1
View File
@@ -10,7 +10,7 @@ fire
fuzzywuzzy fuzzywuzzy
openai openai
litellm>=1.83.14 # to support `from litellm import get_valid_models` litellm>=1.83.14 # to support `from litellm import get_valid_models`
aiohttp>=3.13.5 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525 aiohttp>=3.13.4 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525; >=3.13.4 due to litellm==1.83.14 exact pin
azure.identity azure.identity
pyarrow pyarrow
rich rich