Skip to content

Writing a policy

A policy is a small declarative document attached to one agent. The engine evaluates every tool call against it and returns one of three outcomes.

Each policy rule is shaped like:

permit(principal, action, resource) when { <condition> };
forbid(principal, action, resource) when { <condition> };

Where:

  • principal is the agent — usually Agentum::Agent::"<uuid>".
  • action is the tool name — Action::"call_tool" for tool calls, Action::"http_request" for raw HTTP, etc.
  • resource is what the call is touching — the tool name itself, a URL, a database table.

The engine evaluates all rules. If any forbid matches, the outcome is deny. If at least one permit matches and no forbid matches, the outcome is allow. Otherwise the outcome is deny (deny-by-default).

Allow the agent to call any tool:

permit(principal == Agentum::Agent::"9da9afd6-7158-42b1-ada4-9c84dd4c4d67", action, resource);

Useful for getting started. Not useful in production.

support-bot should be able to read the knowledge base and create tickets, but escalate to a human before deleting one:

// allow read-only KB queries
permit(
principal == Agentum::Agent::"<bot-uuid>",
action == Action::"call_tool",
resource is Agentum::Tool
) when { resource.name == "search_kb" };
// allow ticket creation
permit(
principal == Agentum::Agent::"<bot-uuid>",
action == Action::"call_tool",
resource is Agentum::Tool
) when { resource.name == "create_ticket" };
// require human approval on ticket deletion
@advice("require_hitl:Ticket deletion is irreversible — verify customer intent")
permit(
principal == Agentum::Agent::"<bot-uuid>",
action == Action::"call_tool",
resource is Agentum::Tool
) when { resource.name == "delete_ticket" };
// forbid everything else (deny-by-default already does this, but explicit is clearer)
forbid(
principal == Agentum::Agent::"<bot-uuid>",
action == Action::"call_tool",
resource is Agentum::Tool
) when { !(resource.name in ["search_kb", "create_ticket", "delete_ticket"]) };
  1. Save the policy to a .cedar file:

    Terminal window
    cat > support-bot.cedar <<'EOF'
    permit(principal == Agentum::Agent::"<bot-uuid>", action, resource);
    EOF
  2. Install it via the CLI or the dashboard:

    Terminal window
    agentum policy set <bot-uuid> support-bot.cedar

    Or hit the API directly:

    Terminal window
    curl -X PUT http://localhost:3000/api/v1/policies/<bot-uuid> \
    -H "X-API-Key: $LUPID_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"cedar_source\":$(jq -Rs . support-bot.cedar)}"
  3. Reload so live decisions pick up the change. The API does this automatically when you PUT a policy; if you edited a .cedar file on disk under POLICIES_DIR, you can force a reload:

    Terminal window
    agentum policy reload

The simulate endpoint evaluates a (principal, action, resource) tuple without side effects:

Terminal window
agentum policy simulate \
--agent-id <bot-uuid> \
--action call_tool \
--resource search_kb

Returns allow, deny, or require_hitl along with the matched rule id. Run it for every (action, resource) combination the agent will see during normal operation; anything unexpected here will be a runtime denial in prod.

When you register an agent without supplying a policy:

  • In observe mode (gateway_mode: "observe" in agentum.yaml), Lupid auto-applies a permit-all template scoped to the agent. Useful for staging where you want the audit trail but not the enforcement.
  • In enforce mode (the default), Lupid registers the agent with an empty policy set and logs a warning. Every call denies until you attach a policy.

The engine recognizes a small set of advice annotations on permit rules. The most useful is require_hitl:<reason>, which sends matching calls to the human approval queue before they go out. The reason string is shown to the approver.

Other annotations live in the policy module but the opensource build only wires up require_hitl end-to-end.