Agent Governance Toolkitagent-governance-toolkit 4.1.0 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
23 small wins to finish your pathNext lesson

Checking the arguments, not just the tool

Lesson 5 decided on the tool's name alone. But lookup_order is safe and read_file is safe, right up until the path is /etc/passwd. This lesson goes one level deeper, and finds a trap.

The engine has built-in checks that read the arguments. Allow a file tool, then try to use it on somewhere it should not go.

Example
from agent_control_plane import PolicyEngine

policy = PolicyEngine()
policy.add_constraint("support", ["read_file"])

print(policy.check_violation("support", "read_file", {"path": "notes.txt"}))
print(policy.check_violation("support", "read_file", {"path": "/etc/passwd"}))

The tool was allowed and the call was still refused. The engine keeps a list of protected directories and compares the path against it, after resolving .. so that a path walking upwards out of a safe directory cannot sneak past.

The same idea for queries and commands

Example
print(policy.check_violation("support", "database_query", {"query": "SELECT 1"}))
print(policy.check_violation("support", "database_query", {"query": "DROP TABLE orders"}))

A destructive statement is caught by a pattern, and the refusal names the pattern that fired so you can see which rule you hit rather than guessing.

The trap

Now the same dangerous query, through a tool with a different name. This is the bug worth meeting on purpose.

Example
policy.add_constraint("support", ["database_query", "run_sql"])
dangerous = {"query": "DROP TABLE orders"}

print("database_query:", policy.check_violation("support", "database_query", dangerous))
print("run_sql:       ", policy.check_violation("support", "run_sql", dangerous))

Identical arguments, opposite answers. The second call is allowed, and nothing warned you.

Every one of these built-in checks is keyed on a hardcoded tool name. The path check runs for read_file, write_file and delete_file. The SQL check runs for database_query and database_write. The command check runs for execute_code and run_command. Call your tool anything else and none of them apply.

This is the single most expensive thing to learn late about the policy engine. The argument checks are not general: they are a short list of names with rules attached. A tool called run_sql, query or sql gets no SQL protection at all.

The fix

There are two honest ways out. Name your tools the names the engine already knows, or write the rule yourself. The second is lesson 8; the first costs nothing.

Example
NAMES = {"run_sql": "database_query"}

def check(policy, role, tool, args):
    return policy.check_violation(role, NAMES.get(tool, tool), args)

policy.add_constraint("support", ["database_query"])
print(check(policy, "support", "run_sql", {"query": "DROP TABLE orders"}))

Mapping your own tool name onto the one the engine recognises gets the built-in checks back. It is a small adapter, and it belongs next to the gate rather than scattered through the tools.

Try it yourself
  • Try read_file with a path of safe/../../etc/passwd and confirm the normalisation catches it.
  • Try an api_call with an endpoint starting internal://.
  • Add \n to a path and read the different refusal you get.

Little by little, you're building something great.