mirror of
https://github.com/usestrix/strix.git
synced 2026-08-25 04:12:37 +02:00
Integration fixes the merge required: - guard tools after the strict-schema downgrade, so the copy dataclasses.replace returns is the object the safety wrapper mutates - await _ctx_client, which main made async for the Caido bootstrap handle - pass main's extra_files through with the isolated local sources - keep DEFAULT_SAFETY_MODE alongside main's new report/state imports - rebuild the committed viewer bundle from the merged frontend sources - pin the browser-session safety phrase in test_safety_prompt so it no longer matches unrelated prompt text, and stamp safety_mode on the workspace-file resume record
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""Action-safety guidance reaches the agent only when a safety mode is active."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from strix.agents.prompt import render_system_prompt
|
|
|
|
|
|
# Phrased as prohibitions, so they misdescribe the tools an `off`-mode agent actually has.
|
|
_SAFETY_ONLY_PHRASES = [
|
|
"ACTION SAFETY POLICY",
|
|
"do not override ``--session``",
|
|
"blocked as stale",
|
|
"must be split into a creation call",
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("phrase", _SAFETY_ONLY_PHRASES)
|
|
@pytest.mark.parametrize("context", [None, {}, {"safety_mode": "off"}])
|
|
def test_safety_guidance_is_absent_without_a_safety_mode(
|
|
phrase: str,
|
|
context: dict[str, str] | None,
|
|
) -> None:
|
|
assert phrase not in render_system_prompt(system_prompt_context=context)
|
|
|
|
|
|
@pytest.mark.parametrize("phrase", _SAFETY_ONLY_PHRASES)
|
|
def test_safety_guidance_is_present_in_guarded_mode(phrase: str) -> None:
|
|
assert phrase in render_system_prompt(system_prompt_context={"safety_mode": "guarded"})
|
|
|
|
|
|
def test_browser_skill_carries_no_safety_prohibitions() -> None:
|
|
"""The browser skill is always loaded, so mode-specific rules do not belong in it."""
|
|
prompt = render_system_prompt(skills=["agent_browser"], system_prompt_context={})
|
|
|
|
assert "agent-browser snapshot" in prompt
|
|
for phrase in _SAFETY_ONLY_PHRASES:
|
|
assert phrase not in prompt
|
|
|
|
|
|
def test_guarded_interactive_prompt_explains_human_deferral() -> None:
|
|
prompt = render_system_prompt(
|
|
interactive=True,
|
|
system_prompt_context={
|
|
"safety_mode": "guarded",
|
|
"human_approval_available": True,
|
|
},
|
|
)
|
|
|
|
assert "user approves or denies that action" in prompt
|
|
assert "only the guarded action-safety reviewer may pause" in prompt
|
|
|
|
|
|
def test_guarded_autonomous_prompt_has_no_human_channel() -> None:
|
|
prompt = render_system_prompt(system_prompt_context={"safety_mode": "guarded"})
|
|
|
|
assert "No human approval channel exists" in prompt
|
|
assert "NEVER wait for approval or authorization" in prompt
|
|
|
|
|
|
def test_interactive_without_approval_callback_still_fails_closed() -> None:
|
|
prompt = render_system_prompt(
|
|
interactive=True,
|
|
system_prompt_context={"safety_mode": "guarded"},
|
|
)
|
|
|
|
assert "No human approval channel exists" in prompt
|
|
assert "user approves or denies that action" not in prompt
|
|
|
|
|
|
def test_scope_allows_passive_external_research_without_expanding_targets() -> None:
|
|
prompt = render_system_prompt(
|
|
system_prompt_context={
|
|
"authorized_targets": [{"type": "web", "value": "https://example.test"}],
|
|
"scope_source": "scan",
|
|
"authorization_source": "user",
|
|
}
|
|
)
|
|
|
|
assert "certificate transparency services such as crt.sh" in prompt
|
|
assert "does not make that service a testing target" in prompt
|
|
assert "authorized domain includes its subdomains" in prompt
|
|
assert "NEVER actively scan, fuzz, authenticate to, exploit, or mutate" in prompt
|