Lesson 15 / 28
A Policy Engine Outside the Model
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.
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.
Quick check: What should happen to a tool the policy does not know?
- 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.