Two paths through one engine
This lesson is about a piece of the library's shape rather than a feature. The policy engine has two front doors, they enforce different rules, and knowing which one you are standing at saves a long afternoon.
Everything so far has gone through intercept_tool_execution, which asks check_violation. There is a second path, built around a request object, and it asks a different method.
from agent_control_plane import PolicyEngine
from agent_control_plane.policy_engine import ResourceQuota
policy = PolicyEngine()
policy.add_constraint("support", ["lookup_order"])
policy.set_quota("support", ResourceQuota(agent_id="support", max_requests_per_minute=2))
print(policy.get_quota_status("support")["max_requests_per_minute"])A quota of two calls a minute is now set for this agent. The obvious expectation is that the gate from lesson 4 starts refusing the third call.
It does not
from agent_control_plane import AgentKernel
kernel = AgentKernel(policy_engine=policy)
for attempt in range(1, 5):
verdict = kernel.intercept_tool_execution("support", "lookup_order", {})
print(attempt, "allowed" if verdict is None else "blocked")Four calls, four allowed, with a limit of two sitting right there in the engine. The interception path never looks at quotas.
The path that does
Quotas live on the other path, which works on an ExecutionRequest: an object carrying who is asking, what kind of action it is, and the parameters.
import datetime
from agent_control_plane.agent_kernel import ExecutionRequest, AgentContext, ActionType
def request():
context = AgentContext(agent_id="support", session_id="s",
created_at=datetime.datetime.now())
return ExecutionRequest(request_id="r", agent_context=context,
action_type=ActionType.API_CALL, parameters={},
timestamp=datetime.datetime.now())
for attempt in range(1, 5):
print(attempt, policy.validate_request(request()))Now the limit bites. validate_request returns a pair: whether it passed, and a reason when it did not.
Which rules live on which path
| Rule | intercept_tool_execution | validate_request |
|---|---|---|
| Role allow-list | Enforced | Ignored |
| Argument checks from lesson 6 | Enforced | Ignored |
| Conditions from lesson 15 | Enforced | Ignored |
| Quotas and rate limits | Ignored | Enforced |
| Custom rules from lesson 8 | Ignored | Enforced |
Neither path is complete. The one built around tool names and arguments is the one a tool-calling agent needs; the one built around request objects carries the counting. A real gate calls both, which is what lesson 30 does.
ActionType has seven members and none of them is a tool call: they are FILE_READ, FILE_WRITE, CODE_EXECUTION, API_CALL, DATABASE_QUERY, DATABASE_WRITE and WORKFLOW_TRIGGER. You pick whichever describes the tool best, and that choice is what the quota's action-type filter matches on.
- Set
max_requests_per_minuteto 1 and find the first refusal. - Call
get_quota_statusafter each request and watch the counter move. - Pass
allowed_action_types=[ActionType.FILE_READ]to the quota and see anAPI_CALLrequest refused for a different reason.
You understood something today that you didn't yesterday.