Files
beaver-agent/tests/test_policy.py
T

201 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import tempfile
from pathlib import Path
import pytest
from beaver_gateway.core.policy import Deny, ToolCall
from policy import (
Zones,
bash_zones,
forbid_skills,
requires_skill,
skill_tracker,
vault_zones,
)
@pytest.fixture
def vault() -> Path:
root = Path(tempfile.mkdtemp(prefix="beaver-vault-"))
for d in ("мета/бобер", "💬 чаты", "📅 дни", "👤 люди"):
(root / d).mkdir(parents=True)
(root / "💬 чаты/старый.md").write_text("x")
(root / "📅 дни/2026-08-29.md").write_text("x")
return root
@pytest.fixture
def zones(vault: Path) -> Zones:
return Zones(
vault=vault, write=(vault / "мета/бобер",), create=(vault / "💬 чаты",)
)
def call(
vault: Path, tool: str, state=None, kind: str = "master", **tool_input
) -> ToolCall:
return ToolCall(
tool=tool,
input=tool_input,
agent="a",
kind=kind,
conversation="c",
cwd=vault,
state=state if state is not None else {},
)
def test_write_outside_zones_denied(vault, zones):
rule = vault_zones(zones)
deny = rule(call(vault, "Write", file_path=str(vault / "📅 дни/2026-08-29.md")))
assert isinstance(deny, Deny) and "мета/бобер" in deny.reason
assert rule(call(vault, "Edit", file_path=str(vault / "👤 люди/x.md"))) is not None
assert rule(call(vault, "Write", file_path="новое.md")) is not None
def test_zones_allow_and_create_only(vault, zones):
rule = vault_zones(zones)
assert (
rule(call(vault, "Write", file_path=str(vault / "мета/бобер/дни/x.md"))) is None
)
assert rule(call(vault, "Edit", file_path="мета/бобер/состояние.md")) is None
assert rule(call(vault, "Write", file_path=str(vault / "💬 чаты/новый.md"))) is None
assert (
rule(call(vault, "Write", file_path=str(vault / "💬 чаты/старый.md")))
is not None
)
assert (
rule(call(vault, "Edit", file_path=str(vault / "💬 чаты/новый.md"))) is not None
)
assert rule(call(vault, "Read", file_path=str(vault / "📅 дни/x.md"))) is None
assert rule(call(vault, "Write", file_path="/tmp/scratch.md")) is None
@pytest.mark.parametrize(
"command",
[
"rm '📅 дни/2026-08-29.md'",
"rm -rf 👤\\ люди",
"mv '👤 люди/x.md' 'мета/бобер/x.md'",
"cp мета/бобер/x.md '📅 дни/y.md'",
"echo hi > '📅 дни/today.md'",
"cat a.md | tee '💬 чаты/старый.md'",
"sed -i 's/a/b/' '👤 люди/x.md'",
"cd 👤\\ люди && rm x.md",
"ls; touch new.md",
],
)
def test_bash_mutations_outside_zones_denied(vault, zones, command):
deny = bash_zones(zones)(call(vault, "Bash", command=command))
assert isinstance(deny, Deny), command
assert deny.reason.startswith("Bash: ")
@pytest.mark.parametrize(
"command",
[
"cat '👤 люди/x.md' | grep foo",
"rm мета/бобер/дни/old.md",
"echo hi > мета/бобер/x.md 2>&1",
"cp '👤 люди/x.md' мета/бобер/копия.md",
"sed 's/a/b/' '👤 люди/x.md'",
"echo hi > '💬 чаты/новый.md'",
"rm /tmp/x && ls > /dev/null",
"python3 -c \"print('x')\" >> /tmp/log",
"grep -r 'x' . --include='*.md'",
],
)
def test_bash_reads_and_zone_writes_allowed(vault, zones, command):
assert bash_zones(zones)(call(vault, "Bash", command=command)) is None, command
def test_bash_unbalanced_quotes_fall_back(vault, zones):
assert bash_zones(zones)(call(vault, "Bash", command='rm "📅')) is not None
def test_firefly_requires_open_skill(vault):
state = {}
tracker, gate = (
skill_tracker(),
requires_skill("firefly", ("mcp__firefly__store_*",)),
)
store = call(vault, "mcp__firefly__store_transaction", state=state, data={})
assert isinstance(gate(store), Deny)
assert gate(call(vault, "mcp__firefly__list_account", state=state)) is None
tracker(call(vault, "Skill", state=state, skill="vault:firefly"))
assert gate(store) is None
def test_forbid_skills_only_named_kinds_and_only_skill_tool(vault):
rule = forbid_skills(("firefly", "beaver *"), kinds=("master", "job"))
deny = rule(call(vault, "Skill", skill="vault:firefly"))
assert isinstance(deny, Deny)
assert deny.reason == "скилл firefly недоступен в master: открой ветку"
assert rule(call(vault, "Skill", kind="job", skill="Beaver Durden")) is not None
assert rule(call(vault, "Skill", name="vault:FireFly")) is not None
assert rule(call(vault, "Skill", kind="branch", skill="vault:firefly")) is None
assert rule(call(vault, "Skill", skill="общие:заметки")) is None
assert rule(call(vault, "Read", file_path="скиллы/vault/firefly/SKILL.md")) is None
@pytest.fixture
def home(vault: Path) -> Zones:
"""Зоны диспетчера с 2026-08-30: vault - дом, удаление - только своё."""
(vault / ".obsidian").mkdir()
(vault / "мета/шаблоны").mkdir()
return Zones(
vault=vault,
write=(vault / "мета/бобер",),
create=(vault / "💬 чаты",),
edit=(vault,),
protected=(vault / ".obsidian", vault / "мета"),
)
def test_home_zones_allow_notes_but_protect_obsidian_and_meta(vault, home):
rule = vault_zones(home)
assert rule(call(vault, "Write", file_path=str(vault / "👤 люди/новый.md"))) is None
assert (
rule(call(vault, "Edit", file_path=str(vault / "📅 дни/2026-08-29.md"))) is None
)
assert rule(call(vault, "Write", file_path=str(vault / "мета/бобер/x.md"))) is None
assert (
rule(call(vault, "Edit", file_path=str(vault / "💬 чаты/старый.md")))
is not None
)
deny = rule(call(vault, "Write", file_path=str(vault / ".obsidian/app.json")))
assert isinstance(deny, Deny) and "Бобра" in deny.reason
assert (
rule(call(vault, "Edit", file_path=str(vault / "мета/шаблоны/x.md")))
is not None
)
@pytest.mark.parametrize(
"command",
[
"mv '👤 люди/x.md' '👤 люди/архив/x.md'",
"echo hi >> '📅 дни/2026-08-29.md'",
"sed -i 's/a/b/' '👤 люди/x.md'",
"rm мета/бобер/дни/old.md",
"mkdir '💻 проекты/новый'",
],
)
def test_home_bash_edits_allowed(vault, home, command):
assert bash_zones(home)(call(vault, "Bash", command=command)) is None, command
@pytest.mark.parametrize(
"command",
[
"rm '📅 дни/2026-08-29.md'",
"rm -rf 👤\\ люди",
"truncate -s 0 '👤 люди/x.md'",
"echo x > .obsidian/app.json",
"mv '👤 люди/x.md' мета/шаблоны/x.md",
],
)
def test_home_bash_deletes_and_protected_denied(vault, home, command):
deny = bash_zones(home)(call(vault, "Bash", command=command))
assert isinstance(deny, Deny), command