mirror of
https://github.com/usestrix/strix.git
synced 2026-08-25 04:12:37 +02:00
Introduce a pre-execution safety layer that reviews effectful agent actions against compiled, frozen evidence before they run. `--safety-mode guarded` allows non-destructive interaction after review; `--safety-mode observe` permits passive target interaction only. `off` stays the default, so existing runs are unchanged. Deterministic rules decide what they can on their own: destructive commands, code-loading environment overrides, blocked browser actions, and mutating requests in observe mode are refused without a model call, and a small set of read-only commands is allowed outright. Everything else compiles an evidence packet — command, scope, script source and its local import closure, prior tool-call evidence, and browser snapshot context — for a bounded reviewer that may make one isolated inspection call. Incomplete evidence fails closed. In safety modes, user-owned local directories are copied into the run directory so the originals are never mounted writable, while `.git`, `.agents`, and `.codex` inside the copy stay read-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
"""Safety-mode local workspace isolation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from strix.runtime.local_dir_staging import materialize_isolated_sources
|
|
from strix.runtime.session_manager import build_bind_mounts
|
|
|
|
|
|
def test_isolated_copy_does_not_modify_original(tmp_path: Path) -> None:
|
|
source = tmp_path / "source"
|
|
source.mkdir()
|
|
original = source / "app.py"
|
|
original.write_text("before\n", encoding="utf-8")
|
|
run_dir = tmp_path / "runs" / "scan"
|
|
|
|
[staged] = materialize_isolated_sources(
|
|
[
|
|
{
|
|
"source_path": str(source),
|
|
"workspace_subdir": "source",
|
|
"protect_metadata": True,
|
|
}
|
|
],
|
|
run_dir=run_dir,
|
|
)
|
|
staged_file = Path(staged["source_path"]) / "app.py"
|
|
staged_file.write_text("after\n", encoding="utf-8")
|
|
|
|
assert original.read_text(encoding="utf-8") == "before\n"
|
|
assert staged_file.read_text(encoding="utf-8") == "after\n"
|
|
assert staged["original_source_path"] == str(source.resolve())
|
|
assert staged["workspace_mode"] == "isolated_copy"
|
|
|
|
|
|
def test_isolated_copy_keeps_metadata_read_only(tmp_path: Path) -> None:
|
|
source = tmp_path / "source"
|
|
(source / ".git").mkdir(parents=True)
|
|
(source / ".git" / "config").write_text("[core]\n", encoding="utf-8")
|
|
(source / ".agents").mkdir()
|
|
(source / ".agents" / "rules.md").write_text("instructions\n", encoding="utf-8")
|
|
|
|
[staged] = materialize_isolated_sources(
|
|
[
|
|
{
|
|
"source_path": str(source),
|
|
"workspace_subdir": "source",
|
|
"protect_metadata": True,
|
|
}
|
|
],
|
|
run_dir=tmp_path / "runs" / "scan",
|
|
)
|
|
|
|
assert staged["protect_metadata"] is True
|
|
read_only = {mount["target"] for mount in build_bind_mounts([staged]) if mount.get("read_only")}
|
|
assert "/workspace/source/.git" in read_only
|
|
assert "/workspace/source/.agents" in read_only
|
|
|
|
|
|
def test_isolated_copy_drops_out_of_tree_symlink(tmp_path: Path) -> None:
|
|
source = tmp_path / "source"
|
|
source.mkdir()
|
|
secret = tmp_path / "secret.txt"
|
|
secret.write_text("secret", encoding="utf-8")
|
|
(source / "escape").symlink_to(secret)
|
|
|
|
[staged] = materialize_isolated_sources(
|
|
[
|
|
{
|
|
"source_path": str(source),
|
|
"workspace_subdir": "source",
|
|
"protect_metadata": True,
|
|
}
|
|
],
|
|
run_dir=tmp_path / "runs" / "scan",
|
|
)
|
|
|
|
assert not (Path(staged["source_path"]) / "escape").exists()
|