feat(dangerous-mode): implement real-filesystem access with safety ch… (#276)

* feat(dangerous-mode): implement real-filesystem access with safety checks

- Introduced a 'dangerous mode' allowing the agent to operate on the real filesystem.
- Updated command validation to bypass path confinement while enforcing a blocklist for privileged commands.
- Added warnings and guidelines for users when operating in dangerous mode.
- Enhanced configuration to support dangerous mode and ensure it implies auto-approval.
- Updated tests to verify the behavior of commands and configurations in dangerous mode.

* feat(dangerous-mode): enhance logging and environment management for dangerous mode

* feat(dangerous-mode): improve handling of dangerous mode with environment flags and enhance test isolation
This commit is contained in:
Xi Zhang
2026-06-10 18:19:13 +01:00
committed by GitHub
parent fc05b5eed2
commit c02be519f6
16 changed files with 509 additions and 47 deletions
+70
View File
@@ -28,6 +28,24 @@ from EvoScientist.config import (
# =============================================================================
@pytest.fixture(autouse=True)
def _restore_dangerous_env():
"""Snapshot/restore EVOSCIENTIST_DANGEROUS_MODE around every test.
apply_config_to_env writes this var via direct ``os.environ`` assignment, and
monkeypatch's ``delenv`` of an originally-absent key records no undo — so
without this, a test that turns dangerous mode on would leak the env var into
later tests (an order-dependent landmine, e.g. under pytest-randomly).
"""
_sentinel = object()
_prev = os.environ.get("EVOSCIENTIST_DANGEROUS_MODE", _sentinel)
yield
if _prev is _sentinel:
os.environ.pop("EVOSCIENTIST_DANGEROUS_MODE", None)
else:
os.environ["EVOSCIENTIST_DANGEROUS_MODE"] = _prev
@pytest.fixture
def temp_config_dir(tmp_path, monkeypatch):
"""Use a temporary directory for config during tests."""
@@ -53,6 +71,7 @@ def temp_config_dir(tmp_path, monkeypatch):
"EVOSCIENTIST_AUXILIARY_MODEL",
"EVOSCIENTIST_AUXILIARY_PROVIDER",
"EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE",
"EVOSCIENTIST_DANGEROUS_MODE",
]:
monkeypatch.delenv(key, raising=False)
return config_dir
@@ -75,6 +94,7 @@ def clean_env(monkeypatch):
"EVOSCIENTIST_AUXILIARY_MODEL",
"EVOSCIENTIST_AUXILIARY_PROVIDER",
"EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE",
"EVOSCIENTIST_DANGEROUS_MODE",
]:
monkeypatch.delenv(key, raising=False)
@@ -144,6 +164,18 @@ class TestEvoScientistConfig:
assert config.model == "gpt-4o"
assert config.default_mode == "run"
def test_dangerous_mode_default(self):
"""dangerous_mode defaults off and does not force auto_approve."""
config = EvoScientistConfig()
assert config.dangerous_mode is False
assert config.auto_approve is False
def test_dangerous_mode_implies_auto_approve(self):
"""Enabling dangerous_mode forces auto_approve via __post_init__."""
config = EvoScientistConfig(dangerous_mode=True)
assert config.dangerous_mode is True
assert config.auto_approve is True
# =============================================================================
# Test config path functions
@@ -612,6 +644,44 @@ class TestApplyConfigToEnv:
"true"
)
def test_dangerous_mode_round_trips_to_env(self, clean_env, monkeypatch):
"""dangerous_mode set via CLI override must survive a fresh re-read.
Regression: a --dangerous CLI flag is never persisted to file/env, so a
fresh get_effective_config() (warning banner, run_in_background, the
langgraph dev subprocess) would read it back as False while the backend
is already unconfined. apply_config_to_env round-trips it to env.
"""
from EvoScientist.config import get_effective_config
monkeypatch.delenv("EVOSCIENTIST_DANGEROUS_MODE", raising=False)
cfg = get_effective_config({"dangerous_mode": True})
assert cfg.dangerous_mode is True
apply_config_to_env(cfg)
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") == "true"
# The fresh, no-override read now agrees with the backend.
assert get_effective_config().dangerous_mode is True
def test_dangerous_mode_not_applied_when_off(self, clean_env, monkeypatch):
"""dangerous_mode=False must not write the env var."""
monkeypatch.delenv("EVOSCIENTIST_DANGEROUS_MODE", raising=False)
apply_config_to_env(EvoScientistConfig())
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") is None
def test_dangerous_mode_off_clears_stale_env(self, clean_env, monkeypatch):
"""Re-applying a non-dangerous config clears a previously-set env var.
Regression: the round-trip used to be set-only, so once dangerous mode
was applied in a process it could never be lowered — leaking unconfined
access into later non-dangerous reads.
"""
monkeypatch.delenv("EVOSCIENTIST_DANGEROUS_MODE", raising=False)
apply_config_to_env(EvoScientistConfig(dangerous_mode=True))
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") == "true"
apply_config_to_env(EvoScientistConfig()) # dangerous off
assert os.environ.get("EVOSCIENTIST_DANGEROUS_MODE") is None
def test_ollama_base_url_applied(self, clean_env, monkeypatch):
"""Test that ollama_base_url is applied to OLLAMA_BASE_URL env var."""
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)