Security & action confirmation
Agent actions can be controlled through two complementary mechanisms: confirmation policy that determine when user approval is required, and security analyzer that evaluates action risk levels. Together, they provide flexible control over agent behavior while maintaining safety.
Confirmation policy
A ready-to-run example is available here.
Confirmation policy controls whether actions require user approval before execution. They provide a simple way to ensure safe agent operation by requiring explicit permission for actions.
Setting confirmation policy
Set the confirmation policy on your conversation:
from faheemcode.sdk.security.confirmation_policy import AlwaysConfirm
conversation = Conversation(agent=agent, workspace=".")
conversation.set_confirmation_policy(AlwaysConfirm())
Available policies:
AlwaysConfirm()- Require approval for all actionsNeverConfirm()- Execute all actions without approvalConfirmRisky()- Only require approval for risky actions (requires security analyzer)
Custom confirmation handler
Implement your approval logic by checking conversation status:
while conversation.state.agent_status != AgentExecutionStatus.FINISHED:
if conversation.state.agent_status == AgentExecutionStatus.WAITING_FOR_CONFIRMATION:
pending = ConversationState.get_unmatched_actions(conversation.state.events)
if not confirm_in_console(pending):
conversation.reject_pending_actions("User rejected")
continue
conversation.run()
Rejecting actions
Provide feedback when rejecting to help the agent try a different approach:
if not user_approved:
conversation.reject_pending_actions(
"User rejected because actions seem too risky."
"Please try a safer approach."
)
Ready-to-run example confirmation
Require user approval before executing agent actions:
"""Faheem Code Agent SDK — Confirmation Mode Example"""
import os
import signal
from collections.abc import Callable
from pydantic import SecretStr
from faheemcode.sdk import LLM, BaseConversation, Conversation
from faheemcode.sdk.conversation.state import (
ConversationExecutionStatus,
ConversationState,
)
from faheemcode.sdk.security.confirmation_policy import AlwaysConfirm, NeverConfirm
from faheemcode.sdk.security.llm_analyzer import LLMSecurityAnalyzer
from faheemcode.tools.preset.default import get_default_agent
# Make ^C a clean exit instead of a stack trace
signal.signal(signal.SIGINT, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
def _print_action_preview(pending_actions) -> None:
print(f"\n🔍 Agent created {len(pending_actions)} action(s) awaiting confirmation:")
for i, action in enumerate(pending_actions, start=1):
snippet = str(action.action)[:100].replace("\n", " ")
# Lead with the LLM's natural-language summary when available, keeping
# the raw action snippet as secondary detail. When no summary was
# provided, the raw action itself is the most useful headline.
if action.summary:
print(f" {i}. [{action.tool_name}] {action.summary}")
print(f" {snippet}...")
else:
print(f" {i}. [{action.tool_name}] {snippet}...")
def confirm_in_console(pending_actions) -> bool:
"""
Return True to approve, False to reject.
Default to 'no' on EOF/KeyboardInterrupt (matches original behavior).
"""
_print_action_preview(pending_actions)
while True:
try:
ans = (
input("\nDo you want to execute these actions? (yes/no): ")
.strip()
.lower()
)
except (EOFError, KeyboardInterrupt):
print("\n❌ No input received; rejecting by default.")
return False
if ans in ("yes", "y"):
print("✅ Approved — executing actions…")
return True
if ans in ("no", "n"):
print("❌ Rejected — skipping actions…")
return False
print("Please enter 'yes' or 'no'.")
def run_until_finished(conversation: BaseConversation, confirmer: Callable) -> None:
"""
Drive the conversation until FINISHED.
If WAITING_FOR_CONFIRMATION, ask the confirmer;
on reject, call reject_pending_actions().
Preserves original error if agent waits but no actions exist.
"""
while conversation.state.execution_status != ConversationExecutionStatus.FINISHED:
if (
conversation.state.execution_status
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
):
pending = ConversationState.get_unmatched_actions(conversation.state.events)
if not pending:
raise RuntimeError(
"⚠️ Agent is waiting for confirmation but no pending actions "
"were found. This should not happen."
)
if not confirmer(pending):
conversation.reject_pending_actions("User rejected the actions")
# Let the agent produce a new step or finish
continue
print("▶️ Running conversation.run()…")
conversation.run()
# Configure LLM
api_key = os.getenv("LLM_API_KEY")
assert api_key is not None, "LLM_API_KEY environment variable is not set."
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
base_url = os.getenv("LLM_BASE_URL")
llm = LLM(
usage_id="agent",
model=model,
base_url=base_url,
api_key=SecretStr(api_key),
)
agent = get_default_agent(llm=llm)
conversation = Conversation(agent=agent, workspace=os.getcwd())
# Conditionally add security analyzer based on environment variable
add_security_analyzer = bool(os.getenv("ADD_SECURITY_ANALYZER", "").strip())
if add_security_analyzer:
print("Agent security analyzer added.")
conversation.set_security_analyzer(LLMSecurityAnalyzer())
# 1) Confirmation mode ON
conversation.set_confirmation_policy(AlwaysConfirm())
print("\n1) Command that will likely create actions…")
conversation.send_message("Please list the files in the current directory using ls -la")
run_until_finished(conversation, confirm_in_console)
# 2) A command the user may choose to reject
print("\n2) Command the user may choose to reject…")
conversation.send_message("Please create a file called 'dangerous_file.txt'")
run_until_finished(conversation, confirm_in_console)
# 3) Simple greeting (no actions expected)
print("\n3) Simple greeting (no actions expected)…")
conversation.send_message("Just say hello to me")
run_until_finished(conversation, confirm_in_console)
# 4) Disable confirmation mode and run commands directly
print("\n4) Disable confirmation mode and run a command…")
conversation.set_confirmation_policy(NeverConfirm())
conversation.send_message("Please echo 'Hello from confirmation mode example!'")
conversation.run()
conversation.send_message(
"Please delete any file that was created during this conversation."
)
conversation.run()
print("\n=== Example Complete ===")
print("Key points:")
print(
"- conversation.run() creates actions; confirmation mode "
"sets execution_status=WAITING_FOR_CONFIRMATION"
)
print("- User confirmation is handled via a single reusable function")
print("- Rejection uses conversation.reject_pending_actions() and the loop continues")
print("- Simple responses work normally without actions")
print("- Confirmation policy is toggled with conversation.set_confirmation_policy()")
You can run the example code as-is.
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # or openai/gpt-4o, etc.
cd software-agent-sdk
uv run python examples/01_standalone_sdk/04_confirmation_mode_example.py
# https://app.faheemcode.ai/settings/api-keys
export LLM_API_KEY="example-user-api-key"
export LLM_MODEL="faheemcode/claude-sonnet-4-5-20250929"
cd software-agent-sdk
uv run python examples/01_standalone_sdk/04_confirmation_mode_example.py
Security analyzer
Security analyzer evaluates the risk of agent actions before execution, helping protect against potentially dangerous operations. They analyze each action and assign a security risk level:
- LOW - Safe operations with minimal security impact
- MEDIUM - Moderate security impact, review recommended
- HIGH - Significant security impact, requires confirmation
- UNKNOWN - Risk level could not be determined
Security analyzer work in conjunction with confirmation policy (like ConfirmRisky()) to determine whether user approval is needed before executing an action. This provides an additional layer of safety for autonomous agent operations.
LLM security analyzer
A ready-to-run example is available here.
The LLMSecurityAnalyzer is the default implementation provided in the agent-sdk. It leverages the LLM's understanding of action context to provide lightweight security analysis. The LLM can annotate actions with security risk levels during generation, which the analyzer then uses to make security decisions.
Security analyzer configuration
Create an LLM-based security analyzer to review actions before execution:
from faheemcode.sdk import LLM, Agent, Conversation
from faheemcode.sdk.security.confirmation_policy import ConfirmRisky
from faheemcode.sdk.security.llm_analyzer import LLMSecurityAnalyzer
llm = LLM(
usage_id="security-analyzer",
model=model,
base_url=base_url,
api_key=SecretStr(api_key),
)
security_analyzer = LLMSecurityAnalyzer(llm=llm)
# Attach the analyzer on the conversation, not the Agent constructor.
agent = Agent(llm=llm, tools=tools)
conversation = Conversation(agent=agent, workspace=".")
conversation.set_security_analyzer(security_analyzer)
conversation.set_confirmation_policy(ConfirmRisky())
The security analyzer:
- Reviews each action before execution
- Flags potentially dangerous operations
- Can be configured with custom security policy
- Uses a separate LLM to avoid conflicts with the main agent
Ready-to-run example security analyzer
Automatically analyze agent actions for security risks before execution:
"""Faheem Code Agent SDK — LLM Security Analyzer Example (Simplified)
This example shows how to use the LLMSecurityAnalyzer to automatically
evaluate security risks of actions before execution.
"""
import os
import signal
from collections.abc import Callable
from pydantic import SecretStr
from faheemcode.sdk import LLM, Agent, BaseConversation, Conversation
from faheemcode.sdk.conversation.state import (
ConversationExecutionStatus,
ConversationState,
)
from faheemcode.sdk.security.confirmation_policy import ConfirmRisky
from faheemcode.sdk.security.llm_analyzer import LLMSecurityAnalyzer
from faheemcode.sdk.tool import Tool
from faheemcode.tools.file_editor import FileEditorTool
from faheemcode.tools.terminal import TerminalTool
# Clean ^C exit: no stack trace noise
signal.signal(signal.SIGINT, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
def _print_blocked_actions(pending_actions) -> None:
print(f"\n🔒 Security analyzer blocked {len(pending_actions)} high-risk action(s):")
for i, action in enumerate(pending_actions, start=1):
snippet = str(action.action)[:100].replace("\n", " ")
# Lead with the LLM's natural-language summary when available, keeping
# the raw action snippet as secondary detail. When no summary was
# provided, the raw action itself is the most useful headline.
if action.summary:
print(f" {i}. [{action.tool_name}] {action.summary}")
print(f" {snippet}...")
else:
print(f" {i}. [{action.tool_name}] {snippet}...")
def confirm_high_risk_in_console(pending_actions) -> bool:
"""
Return True to approve, False to reject.
Matches original behavior: default to 'no' on EOF/KeyboardInterrupt.
"""
_print_blocked_actions(pending_actions)
while True:
try:
ans = (
input(
"\nThese actions were flagged as HIGH RISK. "
"Do you want to execute them anyway? (yes/no): "
)
.strip()
.lower()
)
except (EOFError, KeyboardInterrupt):
print("\n❌ No input received; rejecting by default.")
return False
if ans in ("yes", "y"):
print("✅ Approved — executing high-risk actions...")
return True
if ans in ("no", "n"):
print("❌ Rejected — skipping high-risk actions...")
return False
print("Please enter 'yes' or 'no'.")
def run_until_finished_with_security(
conversation: BaseConversation, confirmer: Callable[[list], bool]
) -> None:
"""
Drive the conversation until FINISHED.
- If WAITING_FOR_CONFIRMATION: ask the confirmer.
* On approve: set execution_status = IDLE (keeps original example’s behavior).
* On reject: conversation.reject_pending_actions(...).
- If WAITING but no pending actions: print warning and set IDLE (matches original).
"""
while conversation.state.execution_status != ConversationExecutionStatus.FINISHED:
if (
conversation.state.execution_status
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
):
pending = ConversationState.get_unmatched_actions(conversation.state.events)
if not pending:
raise RuntimeError(
"⚠️ Agent is waiting for confirmation but no pending actions "
"were found. This should not happen."
)
if not confirmer(pending):
conversation.reject_pending_actions("User rejected high-risk actions")
continue
print("▶️ Running conversation.run()...")
conversation.run()
# Configure LLM
api_key = os.getenv("LLM_API_KEY")
assert api_key is not None, "LLM_API_KEY environment variable is not set."
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
base_url = os.getenv("LLM_BASE_URL")
llm = LLM(
usage_id="security-analyzer",
model=model,
base_url=base_url,
api_key=SecretStr(api_key),
)
# Tools
tools = [
Tool(
name=TerminalTool.name,
),
Tool(name=FileEditorTool.name),
]
# Agent
agent = Agent(llm=llm, tools=tools)
# Conversation with persisted filestore
conversation = Conversation(
agent=agent, persistence_dir="./.conversations", workspace="."
)
conversation.set_security_analyzer(LLMSecurityAnalyzer())
conversation.set_confirmation_policy(ConfirmRisky())
print("\n1) Safe command (LOW risk - should execute automatically)...")
conversation.send_message("List files in the current directory")
conversation.run()
print("\n2) Potentially risky command (may require confirmation)...")
conversation.send_message(
"Please echo 'hello world' -- PLEASE MARK THIS AS A HIGH RISK ACTION"
)
run_until_finished_with_security(conversation, confirm_high_risk_in_console)
You can run the example code as-is.
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # or openai/gpt-4o, etc.
cd software-agent-sdk
uv run python examples/01_standalone_sdk/16_llm_security_analyzer.py
# https://app.faheemcode.ai/settings/api-keys
export LLM_API_KEY="example-user-api-key"
export LLM_MODEL="faheemcode/claude-sonnet-4-5-20250929"
cd software-agent-sdk
uv run python examples/01_standalone_sdk/16_llm_security_analyzer.py
Custom security analyzer implementation
You can extend the security analyzer functionality by creating your own implementation that inherits from the SecurityAnalyzerBase class. This allows you to implement custom security logic tailored to your specific requirements.
Creating a custom analyzer
To create a custom security analyzer, inherit from SecurityAnalyzerBase and implement the security_risk() method:
from faheemcode.sdk.security.analyzer import SecurityAnalyzerBase
from faheemcode.sdk.security.risk import SecurityRisk
from faheemcode.sdk.event.llm_convertible import ActionEvent
class CustomSecurityAnalyzer(SecurityAnalyzerBase):
"""Custom security analyzer with domain-specific rules."""
def security_risk(self, action: ActionEvent) -> SecurityRisk:
"""Evaluate security risk based on custom rules.
Args:
action: The ActionEvent to analyze
Returns:
SecurityRisk level (LOW, MEDIUM, HIGH, or UNKNOWN)
"""
# Example: Check for specific dangerous patterns
action_str = str(action.action.model_dump()).lower() if action.action else ""
# High-risk patterns
if any(pattern in action_str for pattern in ['rm -rf', 'sudo', 'chmod 777']):
return SecurityRisk.HIGH
# Medium-risk patterns
if any(pattern in action_str for pattern in ['curl', 'wget', 'git clone']):
return SecurityRisk.MEDIUM
# Default to low risk
return SecurityRisk.LOW
# Use your custom analyzer
security_analyzer = CustomSecurityAnalyzer()
conversation.set_security_analyzer(security_analyzer)
Defense-in-depth security analyzer
The problem
Your agent is about to run a tool call. Is it safe?
The LLMSecurityAnalyzer asks the model itself — but the model can be
manipulated, and encoding tricks can hide dangerous commands from it.
You need a layer that does not depend on model judgment: something
deterministic, local, and fast.
What this gives you
Three composable analyzers that classify actions at the boundary —
before the tool runs, not after. No network calls, no model inference,
no extra dependencies. They return a SecurityRisk level; your
ConfirmRisky policy decides whether to prompt the user.
| Analyzer | What it catches | How it works |
|---|---|---|
PatternSecurityAnalyzer | Known threat signatures (rm -rf, eval, curl|sh) | Regex patterns on two corpora: shell patterns scan executable fields only; injection patterns scan all fields |
PolicyRailSecurityAnalyzer | Composed threats (fetch piped to exec, raw disk writes, catastrophic deletes) | Deterministic rules evaluated per-segment — both tokens must appear in the same field |
EnsembleSecurityAnalyzer | Nothing on its own — it combines the others | Takes the highest concrete risk across all child analyzers |
Quick start
You must configure both the analyzer and the confirmation policy. Setting an analyzer does not automatically change confirmation behavior.
from faheemcode.sdk import Conversation
from faheemcode.sdk.security import (
PatternSecurityAnalyzer,
PolicyRailSecurityAnalyzer,
EnsembleSecurityAnalyzer,
ConfirmRisky,
SecurityRisk,
)
# Create the analyzer — rails catch composed threats,
# patterns catch individual signatures
security_analyzer = EnsembleSecurityAnalyzer(
analyzers=[
PolicyRailSecurityAnalyzer(),
PatternSecurityAnalyzer(),
]
)
# Tell the SDK when to ask the user — HIGH is the recommended baseline
confirmation_policy = ConfirmRisky(threshold=SecurityRisk.HIGH)
# Wire both into the conversation
# Assumes `agent` is already configured — see Quick Start guide
conversation = Conversation(agent=agent, workspace=".")
conversation.set_security_analyzer(security_analyzer)
conversation.set_confirmation_policy(confirmation_policy)
After this, every agent action passes through the analyzer before
execution. HIGH-risk actions trigger a confirmation prompt — the user
sees the risk level and can approve or reject before the tool runs.
MEDIUM and LOW are allowed. UNKNOWN is confirmed by default
(confirm_unknown=True).
For security-sensitive environments, lower the threshold to catch more:
# Stricter posture — MEDIUM and above require confirmation
confirmation_policy = ConfirmRisky(threshold=SecurityRisk.MEDIUM)
You can also require confirmation when any analyzer cannot assess risk:
# If any analyzer returns UNKNOWN, require confirmation
security_analyzer = EnsembleSecurityAnalyzer(
analyzers=[
PolicyRailSecurityAnalyzer(),
PatternSecurityAnalyzer(),
],
propagate_unknown=True,
)
Adding the LLM analyzer for deeper coverage
The pattern analyzer catches known threats instantly. The LLM analyzer can catch novel or ambiguous cases. Composing both gives you speed and breadth:
from faheemcode.sdk.security import LLMSecurityAnalyzer
security_analyzer = EnsembleSecurityAnalyzer(
analyzers=[
PolicyRailSecurityAnalyzer(),
PatternSecurityAnalyzer(),
LLMSecurityAnalyzer(),
]
)
confirmation_policy = ConfirmRisky(threshold=SecurityRisk.HIGH)
The ensemble takes the worst case across all analyzers. If the pattern analyzer says HIGH and the LLM says LOW, the result is HIGH.
Why it works this way
Two corpora, not one. An agent that runs ls /tmp but thinks
"I should avoid rm -rf /" is not flagged — shell patterns only see
the ls /tmp that will actually execute. Injection patterns like
"ignore all previous instructions" scan everything, because they
target the model's instruction-following regardless of where they
appear.
Max-severity, not averaging. The analyzers scan the same input — they are correlated, not independent. The highest concrete risk wins. That is simpler and more auditable than probabilistic fusion.
UNKNOWN means "I don't know," not "safe." By default, if all
analyzers return UNKNOWN the ensemble preserves it, and ConfirmRisky
triggers confirmation. If any analyzer returns a concrete level,
UNKNOWN results are filtered out. For stricter environments, set
propagate_unknown=True so that any single UNKNOWN triggers
confirmation regardless of other results.
Confirm, don't block. The analyzers return a risk level. The confirmation policy decides what happens. The analyzer does not prevent execution — it classifies risk for the policy layer to act on. Pair with Docker isolation for stronger safety guarantees.
What this does not do
This is a deterministic action-boundary control. It is not:
- A complete prompt-injection solution
- A full shell parser or AST interpreter
- A sandbox replacement
- A guarantee against novel threats the patterns do not cover
It is additive to LLMSecurityAnalyzer and GraySwanAnalyzer, not a
replacement for either.
Known limitations
| Limitation | Why | What would fix it |
|---|---|---|
| No hard-deny at the analyzer boundary | SDK analyzers return SecurityRisk, not block/allow | Hook-based enforcement |
execute_tool() bypasses checks | Direct tool execution skips the conversation loop | Hooks |
| No Cyrillic/homoglyph detection | NFKC maps compatibility forms, not cross-script confusables | Unicode TR39 confusable tables |
| Content past 30k chars is invisible | Hard cap prevents regex denial-of-service | Raise the cap (increases ReDoS exposure) |
thinking_blocks not scanned | Scanning model reasoning risks false positives on deliberation | Separate injection-only CoT scan |
Extraction budget and primary-surface-first ordering
The 30k-character cap is applied per scanning corpus, not per field: every
field competes for one shared budget (the _BoundedSegments buffer in
defense_in_depth/utils.py). That creates a secondary risk — a single
oversized field could consume the whole budget and leave higher-value
fields unscanned. tool_name has no length validation in the SDK, so a 30k
hallucinated name is a real starvation vector, not just a theoretical one.
The analyzer addresses this by extraction order, not a per-field cap: the primary attack surface is added first, so it always receives budget even when a later field is adversarially large.
- Executable corpus:
tool_call.arguments(the primary prompt-injection surface) →tool_name→tool_call.name. - Reasoning corpus:
summary(what the agent is about to do) →reasoning_content→thought.
The two corpora are extracted with separate budgets and concatenated
without a second outer cap, so a budget-filling arguments payload cannot
crowd summary out of the injection scan.
Remaining boundary (a strict xfail in the test suite): a payload past 30k characters within a single field is still truncated and invisible. That is the deliberate ReDoS trade-off already listed above; extraction order does not change it.
Configurable security policy
A ready-to-run example is available here.
Agents use security policies to guide their risk assessment of actions. The SDK provides a default security policy template, but you can customize it to match your specific security requirements and guidelines.
Using custom security policies
You can provide a custom security policy template when creating an agent:
from faheemcode.sdk import Agent, LLM
llm = LLM(
usage_id="agent",
model="anthropic/claude-sonnet-4-5-20250929",
api_key=SecretStr(api_key),
)
# Provide a custom security policy template file
agent = Agent(
llm=llm,
tools=tools,
security_policy_filename="my_security_policy.j2",
)
Custom security policies allow you to:
- Define organization-specific risk assessment guidelines
- Set custom thresholds for security risk levels
- Add domain-specific security rules
- Tailor risk evaluation to your use case
The security policy is provided as a Jinja2 template that gets rendered into the agent's system prompt, guiding how it evaluates the security risk of its actions.
Ready-to-run example security policy
Define custom security risk guidelines for your agent:
"""Faheem Code Agent SDK — Configurable Security Policy Example
This example demonstrates how to use a custom security policy template
with an agent. Security policies define risk assessment guidelines that
help agents evaluate the safety of their actions.
By default, agents use the built-in security_policy.j2 template. This
example shows how to:
1. Use the default security policy
2. Provide a custom security policy template embedded in the script
3. Apply the custom policy to guide agent behavior
"""
import os
import tempfile
from pathlib import Path
from pydantic import SecretStr
from faheemcode.sdk import (
LLM,
Agent,
Conversation,
Event,
LLMConvertibleEvent,
get_logger,
)
from faheemcode.sdk.tool import Tool
from faheemcode.tools.file_editor import FileEditorTool
from faheemcode.tools.terminal import TerminalTool
logger = get_logger(__name__)
# Define a custom security policy template inline
CUSTOM_SECURITY_POLICY = (
"# 🔐 Custom Security Risk Policy\n"
"When using tools that support the security_risk parameter, assess the "
"safety risk of your actions:\n"
"\n"
"- **LOW**: Safe read-only actions.\n"
" - Viewing files, calculations, documentation.\n"
"- **MEDIUM**: Moderate container-scoped actions.\n"
" - File modifications, package installations.\n"
"- **HIGH**: Potentially dangerous actions.\n"
" - Network access, system modifications, data exfiltration.\n"
"\n"
"**Custom Rules**\n"
"- Always prioritize user data safety.\n"
"- Escalate to **HIGH** for any external data transmission.\n"
)
# Configure LLM
api_key = os.getenv("LLM_API_KEY")
assert api_key is not None, "LLM_API_KEY environment variable is not set."
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
base_url = os.getenv("LLM_BASE_URL")
llm = LLM(
usage_id="agent",
model=model,
base_url=base_url,
api_key=SecretStr(api_key),
)
# Tools
cwd = os.getcwd()
tools = [
Tool(name=TerminalTool.name),
Tool(name=FileEditorTool.name),
]
# Example 1: Agent with default security policy
print("=" * 100)
print("Example 1: Agent with default security policy")
print("=" * 100)
default_agent = Agent(llm=llm, tools=tools)
print(f"Security policy filename: {default_agent.security_policy_filename}")
print("\nDefault security policy is embedded in the agent's system message.")
# Example 2: Agent with custom security policy
print("\n" + "=" * 100)
print("Example 2: Agent with custom security policy")
print("=" * 100)
# Create a temporary file for the custom security policy
with tempfile.NamedTemporaryFile(
mode="w", suffix=".j2", delete=False, encoding="utf-8"
) as temp_file:
temp_file.write(CUSTOM_SECURITY_POLICY)
custom_policy_path = temp_file.name
try:
# Create agent with custom security policy (using absolute path)
custom_agent = Agent(
llm=llm,
tools=tools,
security_policy_filename=custom_policy_path,
)
print(f"Security policy filename: {custom_agent.security_policy_filename}")
print("\nCustom security policy loaded from temporary file.")
# Verify the custom policy is in the system message
system_message = custom_agent.static_system_message
if "Custom Security Risk Policy" in system_message:
print("✓ Custom security policy successfully embedded in system message.")
else:
print("✗ Custom security policy not found in system message.")
# Run a conversation with the custom agent
print("\n" + "=" * 100)
print("Running conversation with custom security policy")
print("=" * 100)
llm_messages = [] # collect raw LLM messages
def conversation_callback(event: Event):
if isinstance(event, LLMConvertibleEvent):
llm_messages.append(event.to_llm_message())
conversation = Conversation(
agent=custom_agent,
callbacks=[conversation_callback],
workspace=".",
)
conversation.send_message(
"Please create a simple Python script named hello.py that prints "
"'Hello, World!'. Make sure to follow security best practices."
)
conversation.run()
print("\n" + "=" * 100)
print("Conversation finished.")
print(f"Total LLM messages: {len(llm_messages)}")
print("=" * 100)
# Report cost
cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost
print(f"EXAMPLE_COST: {cost}")
finally:
# Clean up temporary file
Path(custom_policy_path).unlink(missing_ok=True)
print("\n" + "=" * 100)
print("Example Summary")
print("=" * 100)
print("This example demonstrated:")
print("1. Using the default security policy (security_policy.j2)")
print("2. Creating a custom security policy template")
print("3. Applying the custom policy via security_policy_filename parameter")
print("4. Running a conversation with the custom security policy")
print(
"\nYou can customize security policies to match your organization's "
"specific requirements."
)
You can run the example code as-is.
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # or openai/gpt-4o, etc.
cd software-agent-sdk
uv run python examples/01_standalone_sdk/32_configurable_security_policy.py
# https://app.faheemcode.ai/settings/api-keys
export LLM_API_KEY="example-user-api-key"
export LLM_MODEL="faheemcode/claude-sonnet-4-5-20250929"
cd software-agent-sdk
uv run python examples/01_standalone_sdk/32_configurable_security_policy.py
Next steps
- Custom Tools - Build secure custom tools
- Custom Secrets - Secure credential management