Skip to content

Commit 040e162

Browse files
Add configurable security policy filename support
- Add security_policy_filename field to Agent model with default 'security_policy.j2' - Update system_prompt.j2 to use configurable security policy template - Add comprehensive tests for security policy configuration - Add example demonstrating configurable security policy usage - All tests pass and pre-commit hooks validated Co-authored-by: openhands <openhands@all-hands.dev>
1 parent fa470e3 commit 040e162

File tree

5 files changed

+274
-1
lines changed

5 files changed

+274
-1
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example demonstrating configurable security policy support in OpenHands Agent.
4+
5+
This example shows how to:
6+
1. Use the default security policy
7+
2. Configure a custom security policy template
8+
3. Verify the security policy is included in the system message
9+
"""
10+
11+
import os
12+
from pathlib import Path
13+
14+
from pydantic import SecretStr
15+
16+
from openhands.sdk.agent import Agent
17+
from openhands.sdk.llm import LLM
18+
19+
20+
def main():
21+
"""Demonstrate configurable security policy functionality."""
22+
print("=== OpenHands Agent: Configurable Security Policy Example ===\n")
23+
24+
# Example 1: Default security policy
25+
print("1. Creating agent with default security policy...")
26+
llm = LLM(
27+
model="gpt-4o-mini",
28+
api_key=SecretStr(os.getenv("OPENAI_API_KEY", "your-api-key-here")),
29+
)
30+
default_agent = Agent(llm=llm)
31+
print(
32+
f" Default security policy filename: {default_agent.security_policy_filename}"
33+
)
34+
35+
# Example 2: Custom security policy
36+
print("\n2. Creating agent with custom security policy...")
37+
38+
# Get the path to our custom policy template
39+
example_dir = Path(__file__).parent
40+
custom_policy_path = example_dir / "custom_policy.j2"
41+
42+
if not custom_policy_path.exists():
43+
print(f" Error: Custom policy template not found at {custom_policy_path}")
44+
return
45+
46+
custom_agent = Agent(
47+
llm=llm,
48+
security_policy_filename="custom_policy.j2",
49+
)
50+
print(
51+
f" Custom security policy filename: {custom_agent.security_policy_filename}"
52+
)
53+
54+
# Example 3: Demonstrate configuration
55+
print("\n3. Configuration summary...")
56+
print(f" Default agent security policy: {default_agent.security_policy_filename}")
57+
print(f" Custom agent security policy: {custom_agent.security_policy_filename}")
58+
print(" ✅ Security policy filename successfully configured!")
59+
60+
# Note: The security policy template is automatically included in the agent's
61+
# system message when the agent processes conversations. The template is
62+
# rendered using Jinja2 and included in the SECURITY_RISK_ASSESSMENT section.
63+
64+
print("\n=== Example completed successfully! ===")
65+
print("\nKey takeaways:")
66+
print("• Agents use 'security_policy.j2' as the default security policy template")
67+
print(
68+
"• You can specify a custom security policy using the "
69+
"security_policy_filename parameter"
70+
)
71+
print("• The security policy template is included in the agent's system message")
72+
print(
73+
"• Custom policies allow you to define organization-specific "
74+
"security guidelines"
75+
)
76+
77+
78+
if __name__ == "__main__":
79+
main()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# 🔐 Custom Security Risk Policy
2+
When using tools that support the security_risk parameter, assess the safety risk of your actions:
3+
4+
- **LOW**: Safe read-only actions.
5+
- Viewing files, calculations, documentation.
6+
- **MEDIUM**: Moderate container-scoped actions.
7+
- File modifications, package installations.
8+
- **HIGH**: Potentially dangerous actions.
9+
- Network access, system modifications, data exfiltration.
10+
11+
**Custom Rules**
12+
- Always prioritize user data safety.
13+
- Escalate to **HIGH** for any external data transmission.

openhands/sdk/agent/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
104104
],
105105
)
106106
system_prompt_filename: str = Field(default="system_prompt.j2")
107+
security_policy_filename: str = Field(
108+
default="security_policy.j2",
109+
description=(
110+
"Filename of the security policy template to include in the system prompt."
111+
),
112+
examples=["security_policy.j2", "custom_security_policy.j2"],
113+
)
107114
system_prompt_kwargs: dict = Field(
108115
default_factory=dict,
109116
description="Optional kwargs to pass to the system prompt Jinja2 template.",
@@ -156,6 +163,9 @@ def system_message(self) -> str:
156163
if hasattr(self, "cli_mode"):
157164
template_kwargs["cli_mode"] = getattr(self, "cli_mode")
158165

166+
# Add security_policy_filename to template kwargs
167+
template_kwargs["security_policy_filename"] = self.security_policy_filename
168+
159169
system_message = render_template(
160170
prompt_dir=self.prompt_dir,
161171
template_name=self.system_prompt_filename,

openhands/sdk/agent/prompts/system_prompt.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Your primary role is to assist users by executing commands, modifying code, and
6767
</SECURITY>
6868

6969
<SECURITY_RISK_ASSESSMENT>
70-
{% include 'security_risk_assessment.j2' %}
70+
{% include security_policy_filename %}
7171
</SECURITY_RISK_ASSESSMENT>
7272

7373
<EXTERNAL_SERVICES>
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""Integration tests for configurable security policy functionality."""
2+
3+
import shutil
4+
import tempfile
5+
from pathlib import Path
6+
7+
import pytest
8+
from pydantic import SecretStr
9+
10+
from openhands.sdk.agent import Agent
11+
from openhands.sdk.llm import LLM
12+
13+
14+
@pytest.fixture
15+
def temp_dir():
16+
"""Create a temporary directory for test files."""
17+
temp_dir = tempfile.mkdtemp()
18+
yield temp_dir
19+
shutil.rmtree(temp_dir)
20+
21+
22+
@pytest.fixture
23+
def test_llm():
24+
"""Create a test LLM instance."""
25+
return LLM(
26+
model="gpt-4o-mini",
27+
api_key=SecretStr("test-key"),
28+
base_url="http://test",
29+
)
30+
31+
32+
@pytest.fixture
33+
def custom_policy_content():
34+
"""Sample custom security policy content."""
35+
return (
36+
"# 🔐 Custom Security Risk Policy\n"
37+
"When using tools that support the security_risk parameter, "
38+
"assess the safety risk of your actions:\n"
39+
"\n"
40+
"- **LOW**: Safe read-only actions.\n"
41+
" - Viewing files, calculations, documentation.\n"
42+
"- **MEDIUM**: Moderate container-scoped actions.\n"
43+
" - File modifications, package installations.\n"
44+
"- **HIGH**: Potentially dangerous actions.\n"
45+
" - Network access, system modifications, data exfiltration.\n"
46+
"\n"
47+
"**Custom Rules**\n"
48+
"- Always prioritize user data safety.\n"
49+
"- Escalate to **HIGH** for any external data transmission."
50+
)
51+
52+
53+
def test_default_security_policy_filename(test_llm):
54+
"""Test that Agent uses default security policy filename."""
55+
agent = Agent(llm=test_llm)
56+
assert agent.security_policy_filename == "security_policy.j2"
57+
58+
59+
def test_custom_security_policy_filename(test_llm):
60+
"""Test that Agent accepts custom security policy filename."""
61+
agent = Agent(
62+
llm=test_llm,
63+
security_policy_filename="custom_policy.j2",
64+
)
65+
assert agent.security_policy_filename == "custom_policy.j2"
66+
67+
68+
def test_security_policy_filename_in_system_message(
69+
temp_dir, custom_policy_content, test_llm, monkeypatch
70+
):
71+
"""Test that custom security policy filename is passed to system message template.""" # noqa: E501
72+
# Create a custom policy file
73+
custom_policy_path = Path(temp_dir) / "custom_policy.j2"
74+
custom_policy_path.write_text(custom_policy_content)
75+
76+
# Create agent with custom security policy
77+
agent = Agent(
78+
llm=test_llm,
79+
security_policy_filename="custom_policy.j2",
80+
)
81+
82+
# Mock the prompt_dir property to point to our temp directory
83+
original_prompt_dir = agent.prompt_dir
84+
85+
def mock_prompt_dir(self):
86+
return temp_dir
87+
88+
monkeypatch.setattr(Agent, "prompt_dir", property(mock_prompt_dir))
89+
90+
# Copy the system_prompt.j2 to temp directory
91+
system_prompt_path = Path(temp_dir) / "system_prompt.j2"
92+
original_system_prompt = Path(original_prompt_dir) / "system_prompt.j2"
93+
if original_system_prompt.exists():
94+
shutil.copy2(original_system_prompt, system_prompt_path)
95+
else:
96+
# Create a minimal system prompt for testing
97+
system_prompt_path.write_text(
98+
"Test system prompt\n<SECURITY_RISK_ASSESSMENT>\n"
99+
"{% include security_policy_filename %}\n</SECURITY_RISK_ASSESSMENT>"
100+
)
101+
102+
# Get system message - this should include our custom policy
103+
system_message = agent.system_message
104+
105+
# Verify that custom policy content appears in system message
106+
assert "Custom Security Risk Policy" in system_message
107+
assert "Always prioritize user data safety" in system_message
108+
109+
110+
def test_configurable_security_policy_filename(test_llm, monkeypatch):
111+
"""Test that security_policy_filename can be configured and is used in template rendering.""" # noqa: E501
112+
# Copy example custom policy to test directory for this test
113+
example_dir = (
114+
Path(__file__).parent.parent.parent.parent / "examples" / "20_security_policy"
115+
)
116+
if not example_dir.exists():
117+
pytest.skip("Example directory not found")
118+
119+
example_policy = example_dir / "custom_policy.j2"
120+
if not example_policy.exists():
121+
pytest.skip("Example custom policy not found")
122+
123+
with tempfile.TemporaryDirectory() as temp_dir:
124+
# Copy the example policy to our test directory
125+
test_policy_path = Path(temp_dir) / "custom_policy.j2"
126+
shutil.copy2(example_policy, test_policy_path)
127+
128+
# Create agent with custom security policy
129+
agent = Agent(
130+
llm=test_llm,
131+
security_policy_filename="custom_policy.j2",
132+
)
133+
134+
# Mock the prompt_dir property to point to our temp directory
135+
original_prompt_dir = agent.prompt_dir
136+
137+
def mock_prompt_dir(self):
138+
return temp_dir
139+
140+
monkeypatch.setattr(Agent, "prompt_dir", property(mock_prompt_dir))
141+
142+
# Copy the system_prompt.j2 to temp directory
143+
system_prompt_path = Path(temp_dir) / "system_prompt.j2"
144+
original_system_prompt = Path(original_prompt_dir) / "system_prompt.j2"
145+
if original_system_prompt.exists():
146+
shutil.copy2(original_system_prompt, system_prompt_path)
147+
148+
# Get system message - this should include our custom policy
149+
system_message = agent.system_message
150+
151+
# Verify that the custom policy content is included
152+
# The exact content will depend on what's in the example custom_policy.j2
153+
assert "SECURITY_RISK_ASSESSMENT" in system_message
154+
155+
156+
def test_security_policy_filename_validation(test_llm):
157+
"""Test that security_policy_filename field accepts valid string values."""
158+
# Test with various valid filenames
159+
valid_filenames = [
160+
"security_policy.j2",
161+
"custom_security_policy.j2",
162+
"my_policy.j2",
163+
"policy_v2.j2",
164+
]
165+
166+
for filename in valid_filenames:
167+
agent = Agent(
168+
llm=test_llm,
169+
security_policy_filename=filename,
170+
)
171+
assert agent.security_policy_filename == filename

0 commit comments

Comments
 (0)