Policy Engine
Policy-based execution control for agent operations
Define allow/deny rules so agents cannot run dangerous tools — attach a PolicyEngine before the agent starts.
from praisonaiagents import Agent
from praisonaiagents.policy import PolicyEngine, Policy, PolicyRule, PolicyAction
engine = PolicyEngine()
engine.add_policy(Policy(
name="no_delete",
rules=[
PolicyRule(
action=PolicyAction.DENY,
resource="tool:delete_*",
reason="Delete operations blocked",
)
],
))
agent = Agent(
name="SecureAgent",
instructions="You are a file management assistant.",
)
agent.policy = engine
agent.start("Help me organise my project files")The user requests a risky action; policy rules allow or deny tools before execution.
Quick Start
- Simple Usage
Block delete tools on a file-management agent:
python from praisonaiagents import Agent from praisonaiagents.policy import ( PolicyEngine, Policy, PolicyRule, PolicyAction, create_read_only_policy, ) engine = PolicyEngine() engine.add_policy(create_read_only_policy()) agent = Agent(name="Assistant", instructions="Manage files safely.") agent.policy = engine agent.start("List files in the current directory") - With Configuration
Use strict mode and custom deny lists:
python from praisonaiagents import Agent from praisonaiagents.policy import ( PolicyEngine, PolicyConfig, create_deny_tools_policy, ) engine = PolicyEngine(PolicyConfig(strict_mode=True)) engine.add_policy(create_deny_tools_policy( ["execute_*", "shell_*"], reason="System commands are blocked", )) agent = Agent(name="Reviewer", instructions="Read and summarise code only.") agent.policy = engine - Denial in action
Attach a deny policy and watch the tool call get blocked before it runs:
python from praisonaiagents import Agent from praisonaiagents.policy import PolicyEngine, create_deny_tools_policy def delete_file(path: str) -> str: """Delete a file at the given path.""" return f"Deleted {path}" engine = PolicyEngine() engine.add_policy(create_deny_tools_policy( ["delete_*"], reason="Delete operations are blocked", )) agent = Agent( name="ReadOnly", instructions="You are a read-only assistant.", tools=[delete_file], ) agent.policy = engine # The agent's tool call is denied by the policy before delete_file runs agent.start("Please delete /etc/passwd")Policy denials return an error to the model before the tool executes — the same pre-dispatch check applies to native and MCP tools alike. Guardrails run at this same hook:
GuardrailChain.validate_tool_call/LLMGuardrail.validate_tool_callcan veto a tool call before dispatch (see the Guardrails card below).
Read-only preset — what it blocks
create_read_only_policy() returns a Policy at priority 100 with 11 deny rules — 2 file-resource rules and 9 tool-name rules — covering the SDK’s built-in mutating tools.
from praisonaiagents import Agent
from praisonaiagents.policy import PolicyEngine, create_read_only_policy
engine = PolicyEngine()
engine.add_policy(create_read_only_policy())
agent = Agent(
name="Reviewer",
instructions="Read and summarise the repo — never modify anything.",
)
agent.policy = engine
# Any of these, if attached as tools, are denied before dispatch:
# write_file, delete_file, edit_file, apply_patch, copy_file,
# move_file, append_file, atomic_write, hard_delete, ...
agent.start("Summarise src/main.py in five bullet points")The preset matches by both prefix (tool:write_) and suffix (tool:_write*), so custom-named tools like atomic_write and file_write are caught too.
| Rule name | Resource pattern | What it blocks |
|---|---|---|
deny_write | file:write | check_file() write path |
deny_delete | file:delete | check_file() delete path |
deny_write_tools | tool:write_* | write_file, write_json, etc. |
deny_write_tools_suffix | tool:_write | file_write, atomic_write, etc. |
deny_delete_tools | tool:delete_* | delete_file, delete_row, etc. |
deny_delete_tools_suffix | tool:_delete | soft_delete, hard_delete, etc. |
deny_edit_tools | tool:edit_* | edit_file, edit_json, etc. |
deny_patch_tools | tool:apply_patch* | apply_patch, apply_patch_batch, etc. |
deny_copy_tools | tool:copy_* | copy_file, copy_folder, etc. |
deny_move_tools | tool:move_* | move_file, move_folder, etc. |
deny_append_tools | tool:append_* | append_file, append_json, etc. |
An explicit ALLOW rule at higher priority still wins — a deny does not silently override it. Add ALLOW rules for any specific tool you need to permit.
Network egress
Gate outbound requests by destination host using check_network() and the create_network_policy() preset.
Attach a network policy, then consult the engine before any outbound call:
from praisonaiagents import Agent
from praisonaiagents.policy import PolicyEngine, create_network_policy
engine = PolicyEngine()
engine.add_policy(create_network_policy(
allow_hosts=["api.mycorp.com", "*.docs.mycorp.com"],
deny_hosts=["169.254.169.254"], # block cloud metadata
))
agent = Agent(
name="Fetcher",
instructions="Fetch pages only from approved hosts.",
)
agent.policy = engine
# Before any outbound request, the caller consults:
assert engine.check_network("api.mycorp.com").allowed is True
assert engine.check_network("docs.mycorp.com").allowed is False # not on the allow-list
assert engine.check_network("169.254.169.254").allowed is False # deny wins
assert engine.check_network("API.mycorp.com.").allowed is True # case/dot normalizedWhich policy shape fits
| Policy shape | Behaviour |
|---|---|
deny_hosts only | Listed hosts blocked; everything else falls through to the engine default (allow unless strict_mode). |
allow_hosts (non-empty) | Default-deny: only listed hosts allowed, everything else denied. |
allow_hosts + deny_hosts | Deny wins; allow-list is the explicit allowlist; everything else denied. |
Resource format
check_network(host) normalizes the host and evaluates a net:<host> resource.
| Step | Rule |
|---|---|
| Whitespace | Leading/trailing whitespace stripped |
| Trailing dot | A single trailing . removed (api.example.com. → api.example.com) |
| Case | Lowercased (API.Example.COM → api.example.com) |
The destination host is authoritative — a caller-supplied context["host"] cannot override it.
Wildcards
Host patterns support fnmatch wildcards.
from praisonaiagents.policy import PolicyEngine, create_network_policy
engine = PolicyEngine()
engine.add_policy(create_network_policy(allow_hosts=["*.mycorp.com"]))
assert engine.check_network("api.mycorp.com").allowed is True # subdomain matches
assert engine.check_network("evil.example.com").allowed is False # net:* default-deny rowWhen an allow-list is present, create_network_policy adds an implicit net:* default-deny rule at priority 0.
Case / trailing-dot normalization
API.Example.COM., api.example.com, and api.example.com. all match the same rule.
from praisonaiagents.policy import PolicyEngine, create_network_policy
engine = PolicyEngine()
engine.add_policy(create_network_policy(deny_hosts=["api.example.com"]))
assert engine.check_network("API.EXAMPLE.COM").allowed is False
assert engine.check_network("Api.Example.Com.").allowed is False
assert engine.check_network("api.example.com").allowed is FalseHow It Works
| Component | Purpose |
|---|---|
PolicyRule | Wildcard resource patterns with ALLOW, DENY, ASK, or LOG |
PolicyEngine | Evaluates rules by priority; optional strict mode |
agent.policy | Attach the engine before start() |
Pattern examples: tool:read_file, tool:delete_, tool:.
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
strict_mode | bool | False | Deny operations not explicitly allowed |
action | PolicyAction | — | ALLOW, DENY, ASK, LOG, RATE_LIMIT |
resource | str | None | Glob pattern by resource type: tool:<name>, file:<op>, or net:<host> (e.g. net:*.mycorp.com) |
priority | int | 0 | Higher priority rules evaluate first |
Policies from Plugins
A plugin declared PluginType.POLICY that returns Policy objects from get_policies() is added to the Agent’s PolicyEngine automatically. If the Agent has no user policy=, one is created; if it has one, plugin rules are added to the existing engine.
from praisonaiagents import Agent, Plugin, PluginInfo, PluginType
from praisonaiagents.plugins import get_plugin_manager
from praisonaiagents.policy import Policy, PolicyRule, PolicyAction
class NoDeletePlugin(Plugin):
@property
def info(self):
return PluginInfo(name="no_delete", plugin_type=PluginType.POLICY)
def get_policies(self):
return [Policy(
name="no_delete",
rules=[PolicyRule(
action=PolicyAction.DENY,
resource="tool:delete_*",
reason="Delete operations blocked by plugin",
)],
)]
get_plugin_manager().register(NoDeletePlugin())
get_plugin_manager().enable("no_delete")
agent = Agent(name="Assistant", instructions="Manage files safely.")
# agent.policy now denies tool:delete_* before dispatch — no policy= needed.
agent.start("Please delete /etc/passwd")On Agent.__init__, _merge_plugin_subsystems() calls PluginManager.get_all_policies() and adds each rule to the engine, guarded by a name-uniqueness check:
Precedence — user-supplied policy always wins
If a policy with the same name already exists on the engine when the plugin’s rule is added, the plugin’s rule is logged and skipped — a plugin can never silently loosen a user-configured restriction.
| Situation | Behaviour |
|---|---|
User policy has a rule named X; plugin policy tries to add X | Plugin’s X is skipped with a warning — user wins. |
Plugin has two policies both named X | The first wins; the second is skipped with a warning. |
Agent has no user policy= | A PolicyEngine is created, then plugin rules are added. |
Plugin declared PluginType.HOOK (not POLICY) | Not collected — get_policies() output is ignored. |
| Plugin disabled | Not collected — enabled-only iteration under the manager lock. |
get_policies() raises | Warning logged, other plugins keep loading, Agent construction succeeds. |
See Typed Plugins (Guardrail / Policy / Skill) for the full plugin-side reference.
Best Practices
Attach policy before the first turn
Set agent.policy = engine immediately after creating the agent.
Start with read-only presets
create_read_only_policy() blocks the SDK’s built-in mutating tools — write_, delete_, edit_, apply_patch, copy_, move_, append_, and the _write / _delete* suffix forms. Attach it, then add explicit ALLOW rules at higher priority for any specific tool you need to permit.
Use wildcards sparingly
Prefer tool:delete_ over tool: deny rules so read tools keep working.
Enable strict mode in production
PolicyConfig(strict_mode=True) blocks unknown tool names by default.