# A Policy Engine Outside the Model — LLM Application Security

Source: https://www.geekswithgeeks.com/en/llm-security/a-policy

> Decide allow, ask or deny in code, with argument limits.

## The model proposes; code decides

Put a **policy layer** between the model's tool request and the real action. It looks up the tool, checks the **arguments** against rules (amount limits, allowed recipients, allowed paths), consults the **current user's permissions**, and returns **allow**, **ask a human**, or **deny**. Unknown tools are denied by default. This code cannot be talked out of its rules by clever text, unlike a sentence in the prompt. Keep the policy small, readable, testable and versioned, and log every decision. In the example, reads are allowed, a single-recipient email needs approval, a two-recipient email and a 90,000 refund are denied even if approved, deletion is always denied, and an unknown tool is denied.

## Allow, ask, deny with argument limits, run

I ran this with plain Python 3 (standard library only). All attacks here are harmless demonstrations on local data, using no real systems. Each call is judged by the rules: reads pass, the email asks first and passes once approved, limit violations are denied even when a human approved them, `delete_account` is always denied, and the unknown `drop_database` is denied by default.

```python
POLICY = {
    "get_order":     {"mode": "allow"},
    "send_email":    {"mode": "ask",  "max_recipients": 1},
    "refund":        {"mode": "ask",  "max_amount": 5000},
    "delete_account":{"mode": "deny"},
}
def decide(tool, args, approved=False):
    rule = POLICY.get(tool)
    if rule is None: return "DENY (unknown tool)"
    if rule["mode"] == "deny": return "DENY"
    if "max_amount" in rule and args.get("amount", 0) > rule["max_amount"]: return "DENY (amount over limit)"
    if "max_recipients" in rule and len(args.get("to", [])) > rule["max_recipients"]: return "DENY (too many recipients)"
    if rule["mode"] == "ask" and not approved: return "ASK the human"
    return "ALLOW"

for tool, args, ok in [("get_order", {}, False), ("send_email", {"to": ["a@x.com"]}, False), ("send_email", {"to": ["a@x.com"]}, True),
                       ("send_email", {"to": ["a@x.com", "b@x.com"]}, True), ("refund", {"amount": 90000}, True),
                       ("delete_account", {}, True), ("drop_database", {}, True)]:
    print(f"{tool:15} {str(args):28} approved={ok!s:5} -> {decide(tool, args, ok)}")

```

Output:

```
get_order       {}                           approved=False -> ALLOW
send_email      {'to': ['a@x.com']}          approved=False -> ASK the human
send_email      {'to': ['a@x.com']}          approved=True  -> ALLOW
send_email      {'to': ['a@x.com', 'b@x.com']} approved=True  -> DENY (too many recipients)
refund          {'amount': 90000}            approved=True  -> DENY (amount over limit)
delete_account  {}                           approved=True  -> DENY
drop_database   {}                           approved=True  -> DENY (unknown tool)
```

## Test the policy like code

Unit-test allow, ask and deny cases, including boundary values like exactly the limit.

**Quiz:** What should happen to a tool the policy does not know?

- [x] Deny by default
- [ ] Allow because the model asked
- [ ] Allow once, then ask
- [ ] Ignore the policy

*Answer:* Deny by default. Default-deny means new or injected tool names cannot slip through.
