Skip to content

Commit 27b1c1c

Browse files
feat: Add configurable security policy support to Agent
This commit adds support for configurable security policies in the Agent class, allowing users to customize the security guidelines included in the system prompt. Key changes: - Added security_policy_filename field to Agent class with default 'security_policy.j2' - Updated system_prompt.j2 to use configurable security policy template - Added comprehensive tests for the new functionality - Created working example demonstrating usage Files changed: - openhands/sdk/agent/base.py: Added security_policy_filename field - openhands/sdk/agent/prompts/system_prompt.j2: Updated to use configurable policy - tests/sdk/agent/test_security_policy_integration.py: Added comprehensive tests - examples/20_security_policy/configurable_security_policy.py: Working example - examples/20_security_policy/custom_policy.j2: Example custom policy template Co-authored-by: openhands <[email protected]>
1 parent e86d0a7 commit 27b1c1c

File tree

5 files changed

+266
-1
lines changed

5 files changed

+266
-1
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example: Configurable Security Policy
4+
5+
This example demonstrates how to use the security_policy_filename field
6+
to customize the security policy included in agent system prompts.
7+
8+
The security policy is a critical component that defines what actions
9+
the agent is allowed to perform and under what conditions. By making
10+
it configurable, you can:
11+
12+
1. Use different security policies for different environments
13+
2. Customize security rules for specific use cases
14+
3. Implement organization-specific security guidelines
15+
4. Test with relaxed or stricter security policies
16+
17+
Usage:
18+
python examples/20_security_policy/configurable_security_policy.py
19+
"""
20+
21+
from pathlib import Path
22+
23+
from pydantic import SecretStr
24+
25+
from openhands.sdk import LLM
26+
from openhands.sdk.agent import Agent
27+
28+
29+
def setup_example_security_policy():
30+
"""Set up the example custom security policy template."""
31+
# Get the path to the SDK prompts directory
32+
current_dir = Path(__file__).parent
33+
sdk_dir = current_dir.parent.parent / "openhands" / "sdk" / "agent" / "prompts"
34+
35+
# Read the example custom security policy
36+
example_policy_path = current_dir / "custom_policy.j2"
37+
target_policy_path = sdk_dir / "custom_policy.j2"
38+
39+
# Copy the example policy to the prompts directory if it doesn't exist
40+
if not target_policy_path.exists():
41+
with open(example_policy_path, "r") as f:
42+
content = f.read()
43+
with open(target_policy_path, "w") as f:
44+
f.write(content)
45+
print(f"Created example security policy at: {target_policy_path}")
46+
47+
return "custom_policy.j2"
48+
49+
50+
def main():
51+
"""Demonstrate configurable security policy functionality."""
52+
print("=== Configurable Security Policy Example ===\n")
53+
54+
# Set up the example security policy
55+
custom_policy_filename = setup_example_security_policy()
56+
57+
# Create LLM instance for testing
58+
llm = LLM(model="test-model", api_key=SecretStr("test-key"), base_url="http://test")
59+
60+
# 1. Agent with default security policy
61+
print("1. Agent with default security policy:")
62+
default_agent = Agent(llm=llm)
63+
print(f" Security policy filename: {default_agent.security_policy_filename}")
64+
65+
# Extract security section from system message
66+
system_message = default_agent.system_message
67+
security_start = system_message.find("<SECURITY>")
68+
security_end = system_message.find("</SECURITY>") + len("</SECURITY>")
69+
security_section = system_message[security_start:security_end]
70+
71+
print(" Security section preview:")
72+
print(f" {security_section[:100]}...")
73+
print()
74+
75+
# 2. Agent with custom security policy
76+
print("2. Agent with custom security policy:")
77+
custom_agent = Agent(llm=llm, security_policy_filename=custom_policy_filename)
78+
print(f" Security policy filename: {custom_agent.security_policy_filename}")
79+
80+
# Extract security section from system message
81+
custom_system_message = custom_agent.system_message
82+
custom_security_start = custom_system_message.find("<SECURITY>")
83+
custom_security_end = custom_system_message.find("</SECURITY>") + len("</SECURITY>")
84+
custom_security_section = custom_system_message[
85+
custom_security_start:custom_security_end
86+
]
87+
88+
print(" Security section preview:")
89+
print(f" {custom_security_section[:100]}...")
90+
print()
91+
92+
# 3. Demonstrate model_copy with different security policy
93+
print("3. Using model_copy to change security policy:")
94+
copied_agent = default_agent.model_copy(
95+
update={"security_policy_filename": custom_policy_filename}
96+
)
97+
print(f" Original agent: {default_agent.security_policy_filename}")
98+
print(f" Copied agent: {copied_agent.security_policy_filename}")
99+
print()
100+
101+
# 4. Show that different policies produce different system messages
102+
print("4. Verification that different policies produce different content:")
103+
default_has_custom = "🔒 Example Custom Security Policy" in system_message
104+
custom_has_custom = "🔒 Example Custom Security Policy" in custom_system_message
105+
106+
print(f" Default agent has example custom policy content: {default_has_custom}")
107+
print(f" Custom agent has example custom policy content: {custom_has_custom}")
108+
109+
default_has_default = "🔐 Security Policy" in system_message
110+
custom_has_default = "🔐 Security Policy" in custom_system_message
111+
112+
print(f" Default agent has default policy content: {default_has_default}")
113+
print(f" Custom agent has default policy content: {custom_has_default}")
114+
115+
print("\n=== Example Complete ===")
116+
print("\nKey takeaways:")
117+
print(
118+
"- The security_policy_filename field allows customization of security policies"
119+
)
120+
print("- Default value is 'security_policy.j2' for backward compatibility")
121+
print("- Different security policies produce different system message content")
122+
print(
123+
"- You can use model_copy() to create agents with different security policies"
124+
)
125+
print("\nTo create your own security policy:")
126+
print("1. Create a new .j2 template file in openhands/sdk/agent/prompts/")
127+
print("2. Define your security rules and guidelines")
128+
print("3. Pass the filename to the Agent constructor")
129+
130+
131+
if __name__ == "__main__":
132+
main()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# 🔒 Example Custom Security Policy
2+
3+
This is a custom security policy template created for demonstration purposes.
4+
It shows how you can create your own security policies for different use cases.
5+
6+
## Custom Security Rules for Example
7+
8+
### Allowed Actions
9+
- Read and analyze files in the current directory
10+
- Perform basic calculations and data processing
11+
- Generate reports and summaries
12+
- Create temporary files for processing
13+
14+
### Restricted Actions
15+
- No network access to external services
16+
- No modification of system files
17+
- No execution of arbitrary shell commands
18+
- No access to sensitive environment variables
19+
20+
### Special Guidelines for Examples
21+
- Always explain what actions are being taken
22+
- Provide clear reasoning for security decisions
23+
- Use safe, predictable operations only
24+
- Focus on educational value over functionality
25+
26+
## Implementation Notes
27+
28+
This custom policy is more restrictive than the default policy and is designed
29+
specifically for educational examples where safety and predictability are
30+
more important than full functionality.
31+
32+
You can create your own security policies by:
33+
1. Creating a new .j2 template file
34+
2. Defining your specific security rules
35+
3. Referencing the template filename when creating agents

openhands/sdk/agent/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
109109
description="Optional kwargs to pass to the system prompt Jinja2 template.",
110110
examples=[{"cli_mode": True}],
111111
)
112+
security_policy_filename: str = Field(
113+
default="security_policy.j2",
114+
description=(
115+
"Filename of the security policy template to include in system prompt."
116+
),
117+
)
112118
security_analyzer: analyzer.SecurityAnalyzerBase | None = Field(
113119
default=None,
114120
description="Optional security analyzer to evaluate action risks.",
@@ -156,6 +162,9 @@ def system_message(self) -> str:
156162
if hasattr(self, "cli_mode"):
157163
template_kwargs["cli_mode"] = getattr(self, "cli_mode")
158164

165+
# Add security_policy_filename to template kwargs
166+
template_kwargs["security_policy_filename"] = self.security_policy_filename
167+
159168
system_message = render_template(
160169
prompt_dir=self.prompt_dir,
161170
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
@@ -62,7 +62,7 @@ Your primary role is to assist users by executing commands, modifying code, and
6262
</PROBLEM_SOLVING_WORKFLOW>
6363

6464
<SECURITY>
65-
{% include 'security_policy.j2' %}
65+
{% include security_policy_filename %}
6666
</SECURITY>
6767

6868
<SECURITY_RISK_ASSESSMENT>

tests/sdk/agent/test_security_policy_integration.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,92 @@ def test_security_policy_template_rendering():
6969
# Verify it's properly formatted (no extra whitespace at start/end)
7070
assert not security_policy.startswith(" ")
7171
assert not security_policy.endswith(" ")
72+
73+
74+
def test_configurable_security_policy_filename():
75+
"""Test that security policy filename can be configured."""
76+
# First, set up the custom policy by copying from examples
77+
import shutil
78+
from pathlib import Path
79+
80+
current_dir = Path(__file__).parent
81+
examples_dir = current_dir.parent.parent.parent / "examples" / "20_security_policy"
82+
prompts_dir = (
83+
current_dir.parent.parent.parent / "openhands" / "sdk" / "agent" / "prompts"
84+
)
85+
86+
# Copy the example custom policy to prompts directory for testing
87+
example_policy = examples_dir / "custom_policy.j2"
88+
test_policy = prompts_dir / "custom_policy.j2"
89+
90+
if example_policy.exists():
91+
shutil.copy(example_policy, test_policy)
92+
93+
try:
94+
# Create agent with custom security policy filename
95+
agent = Agent(
96+
llm=LLM(
97+
model="test-model",
98+
api_key=SecretStr("test-key"),
99+
base_url="http://test",
100+
),
101+
security_policy_filename="custom_policy.j2",
102+
)
103+
104+
# Get the system message
105+
system_message = agent.system_message
106+
107+
# Verify that the custom security policy content is included
108+
assert "🔒 Example Custom Security Policy" in system_message
109+
assert "This is a custom security policy template" in system_message
110+
111+
# Verify that the default security policy content is NOT included
112+
assert "🔐 Security Policy" not in system_message
113+
assert "OK to do without Explicit User Consent" not in system_message
114+
finally:
115+
# Clean up the test file
116+
if test_policy.exists():
117+
test_policy.unlink()
118+
119+
120+
def test_default_security_policy_filename():
121+
"""Test that default security policy filename works correctly."""
122+
# Create agent with default security policy filename
123+
agent = Agent(
124+
llm=LLM(
125+
model="test-model", api_key=SecretStr("test-key"), base_url="http://test"
126+
)
127+
)
128+
129+
# Verify the default filename is set correctly
130+
assert agent.security_policy_filename == "security_policy.j2"
131+
132+
# Get the system message
133+
system_message = agent.system_message
134+
135+
# Verify that the default security policy content is included
136+
assert "🔐 Security Policy" in system_message
137+
assert "OK to do without Explicit User Consent" in system_message
138+
139+
140+
def test_security_policy_filename_field_access():
141+
"""Test that security_policy_filename field can be accessed and modified."""
142+
# Create agent with custom security policy filename
143+
agent = Agent(
144+
llm=LLM(
145+
model="test-model", api_key=SecretStr("test-key"), base_url="http://test"
146+
),
147+
security_policy_filename="custom_security_policy.j2",
148+
)
149+
150+
# Verify the field is accessible
151+
assert agent.security_policy_filename == "custom_security_policy.j2"
152+
153+
# Test model_copy with different security policy filename
154+
agent_copy = agent.model_copy(
155+
update={"security_policy_filename": "security_policy.j2"}
156+
)
157+
assert agent_copy.security_policy_filename == "security_policy.j2"
158+
assert (
159+
agent.security_policy_filename == "custom_security_policy.j2"
160+
) # Original unchanged

0 commit comments

Comments
 (0)