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.

python
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.

Diagram

deny

allow

🤖 Agent

🛡️ PolicyEngine

Rule match?

🚫 Blocked

🔧 Tool runs

Quick Start

  1. 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")
  2. 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
  3. 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_call can 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.

python
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 nameResource patternWhat it blocks
deny_writefile:writecheck_file() write path
deny_deletefile:deletecheck_file() delete path
deny_write_toolstool:write_*write_file, write_json, etc.
deny_write_tools_suffixtool:_writefile_write, atomic_write, etc.
deny_delete_toolstool:delete_*delete_file, delete_row, etc.
deny_delete_tools_suffixtool:_deletesoft_delete, hard_delete, etc.
deny_edit_toolstool:edit_*edit_file, edit_json, etc.
deny_patch_toolstool:apply_patch*apply_patch, apply_patch_batch, etc.
deny_copy_toolstool:copy_*copy_file, copy_folder, etc.
deny_move_toolstool:move_*move_file, move_folder, etc.
deny_append_toolstool: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.

Diagram

deny

allow

🤖 Agent

🛡️ PolicyEngine

check_network(host)

🚫 Blocked

🌐 Request sent

Attach a network policy, then consult the engine before any outbound call:

python
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 normalized

Which policy shape fits

Diagram

Egress goal?

Block specific hosts

Only these hosts

Allowlist + blocklist

deny_hosts=[…]
others fall through

allow_hosts=[…]
default-deny the rest

allow + deny
deny wins

Policy shapeBehaviour
deny_hosts onlyListed 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_hostsDeny wins; allow-list is the explicit allowlist; everything else denied.

Resource format

check_network(host) normalizes the host and evaluates a net:<host> resource.

StepRule
WhitespaceLeading/trailing whitespace stripped
Trailing dotA single trailing . removed (api.example.com. → api.example.com)
CaseLowercased (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.

python
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 row

When 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.

python
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 False

How It Works

Diagram
ToolPolicyEngineAgentToolPolicyEngineAgentalt[denied][allowed]check(“tool:delete_file”)PolicyResult(allowed=false)PolicyResult(allowed=true)execute
ComponentPurpose
PolicyRuleWildcard resource patterns with ALLOW, DENY, ASK, or LOG
PolicyEngineEvaluates rules by priority; optional strict mode
agent.policyAttach the engine before start()

Pattern examples: tool:read_file, tool:delete_, tool:.


Configuration Options

OptionTypeDefaultDescription
strict_modeboolFalseDeny operations not explicitly allowed
actionPolicyAction—ALLOW, DENY, ASK, LOG, RATE_LIMIT
resourcestrNoneGlob pattern by resource type: tool:<name>, file:<op>, or net:<host> (e.g. net:*.mycorp.com)
priorityint0Higher 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.

python
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:

Diagram
PolicyEngineAgent._merge_plugin_subsystemsPluginManagerPOLICY PluginPolicyEngineAgent._merge_plugin_subsystemsPluginManagerPOLICY Pluginskipped with a warningalt[name is free][name exists (user or earlier plugin)]get_policies() → [Policy(“no_delete”)]get_all_policies()[Policy(“no_delete”)]name already present?add_policy(rule)

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.

SituationBehaviour
User policy has a rule named X; plugin policy tries to add XPlugin’s X is skipped with a warning — user wins.
Plugin has two policies both named XThe 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 disabledNot collected — enabled-only iteration under the manager lock.
get_policies() raisesWarning 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.


Related