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

Allowed only when the context agrees

An allow-list answers may this agent ever refund?. Often the real rule is may it refund this, now?. That needs the policy to read something outside the call.

The mechanism is a conditional permission: a tool name plus a list of conditions that all have to hold. The conditions read from a context dictionary you set on the role.

Example
from agent_control_plane import PolicyEngine
from agent_control_plane.policy_engine import ConditionalPermission, Condition

policy = PolicyEngine()
policy.add_conditional_permission("support", ConditionalPermission(
    tool_name="issue_refund",
    conditions=[Condition("customer_status", "equals", "verified")]))
policy.set_agent_context("support", {"customer_status": "verified"})

print(policy.check_violation("support", "issue_refund", {}))

The customer is verified and the call was refused anyway. Nothing raised, nothing was logged, and the condition that should have passed did not.

The reason

equals is not an operator. The engine compares the operator string against a list of names it knows, and when none of them match it falls through to returning False. An unrecognised operator therefore means the condition can never be true.

Example
policy = PolicyEngine()
policy.add_conditional_permission("support", ConditionalPermission(
    tool_name="issue_refund",
    conditions=[Condition("customer_status", "eq", "verified")]))
policy.set_agent_context("support", {"customer_status": "verified"})

print("verified:", policy.check_violation("support", "issue_refund", {}))
policy.update_agent_context("support", {"customer_status": "new"})
print("new:     ", policy.check_violation("support", "issue_refund", {}))

With eq it behaves. The operators are eq, ne, gt, lt, gte, lte, in, not_in and contains, and every one of them is the short form.

This fails shut, which is the safe direction, but it fails shut silently. A policy that refuses everything looks identical to a policy that is working, right up until somebody reports that no refund has gone through since Tuesday. The YAML layer in lesson 10 refuses to load an unknown operator instead, which is why a policy in a file is easier to trust.

A condition that grants

There is a second surprise here, and it runs the other way. Adding a conditional permission also adds the tool to the role's allow-list.

Example
policy = PolicyEngine()
print("before:", policy.check_violation("support", "issue_refund", {}))

policy.add_conditional_permission("support", ConditionalPermission(
    tool_name="issue_refund", conditions=[Condition("customer_status", "eq", "verified")]))
policy.set_agent_context("support", {"customer_status": "verified"})
print("after: ", policy.check_violation("support", "issue_refund", {}))

No add_constraint was called and the tool is now usable. A conditional permission is a grant with a condition attached, not a restriction on a grant made elsewhere.

That matters when you read a policy back. A tool appearing in state_permissions may have been put there by a conditional permission rather than by anyone writing an allow-list, so the two have to be read together.

All, or any

Several conditions default to needing all of them. Pass require_all=False and any one is enough.

Example
policy.add_conditional_permission("support", ConditionalPermission(
    tool_name="send_email",
    conditions=[Condition("customer_status", "eq", "verified"),
                Condition("manager_present", "eq", True)],
    require_all=False))
policy.set_agent_context("support", {"manager_present": True})
print(policy.check_violation("support", "send_email", {}))

Only the second condition holds and the call is allowed. With the default of require_all=True it would have been refused.

Try it yourself
  • Put equals back and add a second, correct condition, to see one bad operator sink the whole permission.
  • Use in with a list of allowed statuses instead of eq.
  • Remove the context entirely with set_agent_context("support", {}) and find out which way a missing attribute fails here.

Every expert started right here.