Configure tool policies
Write a ToolPolicy that decides which tool calls an agent may make, test it in audit mode, then enforce it. The ToolPolicy reference lists every field, and Policy engine architecture explains how the broker decides.
Prerequisites
Section titled “Prerequisites”- Enterprise enabled in the chart (
enterprise.enabled: true). Without a licence, Omnia runs in dev mode with every feature on. See Licensing. - An AgentRuntime whose
spec.toolRegistryRefnames a deployed ToolRegistry, in the same namespace as the ToolPolicy. - For claim-based rules: an AgentRuntime with
spec.externalAuthconfigured, so verified token claims reach the broker. See Configure agent authentication.
Write and apply a policy
Section titled “Write and apply a policy”-
Select the tools. Omit
toolsto select every tool in the registry.spec:selector:registry: customer-toolstools:- process_refund -
Add a producer for each fact the policy needs. A producer is a CEL expression that reads the tool call and returns a bool, string or int. The policy reads the result as
context.<name>.producers:- name: overRefundLimittype: celcel: 'has(body.amount) && double(body.amount) > 500.0' -
Write the Cedar policy. Start with the grant, then add
forbidstatements.policy: |@id("permit-refunds")permit (principal, action == Action::"invoke",resource in Registry::"customer-tools");@id("max-refund-amount")@message("Refund amount exceeds the $500 limit")forbid (principal, action == Action::"invoke", resource)when { context.overRefundLimit };@idbecomesdeniedByon the decision.@messagereaches the model as the tool result, so state what a compliant call looks like. -
Set
mode: auditand apply the manifest. An audit policy never blocks and never grants. The broker records each call it would have denied.apiVersion: omnia.altairalabs.ai/v1alpha1kind: ToolPolicymetadata:name: refund-guardrailsnamespace: productionspec:mode: audit# selector, producers and policy from the steps aboveTerminal window kubectl apply -f refund-guardrails.yamlAn audit policy with no
permitreports only the calls aforbidmatches. One that holds apermitalso reports the calls it would not grant. -
Read the broker log after some agent traffic. Illustrative output:
{"msg":"policy_decision","allowed":true,"deniedBy":"max-refund-amount","message":"Refund amount exceeds the $500 limit","mode":"audit","policy":"refund-guardrails","policyDigest":"sha256:<hex>"}wouldDeny: trueappears in the decision returned to the runtime, not in this log line. -
Change
modetoenforce(the default) and apply again.
An applied edit or delete reaches running agents without a restart.
Write producers
Section titled “Write producers”A producer’s cel reads body, headers, tool, governance and identity. CEL string functions such as startsWith and matches are available.
Guard field access with has(). A producer that errors denies the call under the default onFailure: deny.
producers: - name: overQuantityLimit type: cel cel: 'has(body.quantity) && int(body.quantity) > 100' - name: externalURL type: cel cel: 'has(body.url) && !body.url.startsWith("https://internal.")' - name: destructiveSQL type: cel cel: 'has(body.query) && body.query.matches("(?i)(DROP|DELETE|TRUNCATE)")'Header keys are in canonical form, and every header the broker sees starts with X-Omnia-. Test for a key with in before you index it (illustrative provider name):
producers: - name: usesLocalModel type: cel cel: '"X-Omnia-Provider" in headers && headers["X-Omnia-Provider"] == "ollama"'The broker supplies context.argBytes, context.argNames and context.hasAttachments for every call, so a policy works with no producers. A producer cannot reuse those names. See Built-in context.
A producer yields a bool, int or string, and the controller must be able to tell which from the expression. Convert a bare field read: int(body.quantity), not body.quantity. The controller then checks every context.<name> the policy reads against the producers and built-ins, and puts a policy that misspells a key or compares it with the wrong type in phase Error. Only a producer the policy reads needs a type the controller can tell. See Schema validation.
Require JWT claims
Section titled “Require JWT claims”Require claims with forbid ... unless. A forbid refuses the call whatever any other policy permits. A condition inside a permit does not.
policy: | @id("permit-all") permit (principal, action == Action::"invoke", resource);
@id("require-claims") @message("The team and customer_id claims are required") forbid (principal, action == Action::"invoke", resource) unless { principal.claims has team && principal.claims has customer_id };
@id("finance-only") @message("Only the finance team can use this tool") forbid (principal, action == Action::"invoke", resource) unless { principal.claims has team && principal.claims.team == "finance" };principal.claims is keyed by the lowercased claim name, so write team, not Team. Quote a name that is not a Cedar identifier: principal.claims has "customer-id", read as principal.claims["customer-id"].
Claim values are strings. An array claim arrives comma-joined, and a nested object is dropped. claimMapping maps only subject and endUser; every other claim keeps its name.
Combine a fact with identity: the producer reports what was sent, and the policy decides who may send it.
producers: - name: highValue type: cel cel: 'has(body.amount) && double(body.amount) > 1000.0'policy: | @id("permit-all") permit (principal, action == Action::"invoke", resource);
@id("high-value-finance-only") @message("Only the finance team can process amounts over $1000") forbid (principal, action == Action::"invoke", resource) when { context.highValue && !(principal.claims has team && principal.claims.team == "finance") };Inject headers into upstream requests
Section titled “Inject headers into upstream requests”Headers are added to the tool request only when the policy allows the call. Each rule sets exactly one of value or cel; a rule that sets both or neither puts the policy in phase Error.
spec: headerInjection: - header: X-Policy-Version value: "v2" - header: X-Tenant-Id cel: 'headers["X-Omnia-Claim-Customer_id"]' - header: X-Audit-Trail cel: 'headers["X-Omnia-Agent-Name"] + "/" + headers["X-Omnia-Session-Id"]'A header expression that errors does not change the decision. See headerInjection.
Apply a policy to every tool in a registry
Section titled “Apply a policy to every tool in a registry”Omit tools from the selector. This example refuses any call with an oversized argument payload and needs no producer:
spec: selector: registry: customer-tools policy: | @id("permit-registry") permit (principal, action == Action::"invoke", resource in Registry::"customer-tools");
@id("argument-size-limit") @message("Arguments are limited to 64 KiB") forbid (principal, action == Action::"invoke", resource) when { context.argBytes > 65536 };Verify
Section titled “Verify”List the policies:
kubectl get toolpolicies -n productionEach policy shows phase Active, and RULES counts its Cedar statements (the example in this guide has two). The columns are NAME, READY, REGISTRY, MODE, PHASE, RULES, ADVISORIES and AGE.
ADVISORIES counts the advisory checks that found a likely mistake, such as a non-canonical header key. An advisory never blocks the policy, so phase stays Active. Run kubectl describe to read each condition’s reason and message. The dashboard shows the same advisories on the registry’s Policies tab, and the registry’s Overview shows a warning while any policy that selects the registry has an advisory.
If a policy shows phase Error, describe it to read the compile error:
kubectl describe toolpolicy refund-guardrails -n productionTroubleshooting
Section titled “Troubleshooting”Every call to the tool is denied
Section titled “Every call to the tool is denied”The decision’s deniedBy is no-matching-permit. No permit matched, or no ToolPolicy in the agent’s namespace selects the tool’s registry. Add a permit as in step 3. See Denial response format.
An edit did not take effect
Section titled “An edit did not take effect”kubectl get toolpolicy shows phase Error. A broker that already holds the previous version keeps enforcing it, so a policy still showing your old rule means the new text did not compile or failed schema validation. Fix the error shown by kubectl describe.
Phase is Error with a cedar schema validation message
Section titled “Phase is Error with a cedar schema validation message”The text parses, but a statement does not match what the policy declares. A broker that holds the previous version keeps enforcing it until you fix the statement. The Compiled condition names the statement by its @id and the reason:
context key "piiscore" is not supplied by any producer or built-in: the statement reads or tests a context key that nothing supplies. Check the spelling againstspec.producers[].name; keys are case-sensitive.entity type "User" does not exist: anistest names a type the schema does not declare. UseOmniaPrincipal,Workspace,Agent,RegistryorTool.the types Long and String are not compatible: the statement compares a fact with a value of another type. An int producer is aLong; compare it with a number.unable to guarantee safety of access to optional attribute: the statement reads a claim without testing it first. Writeprincipal.claims has team && principal.claims.team == "finance". A test that holds only through!and||does not count: rewrite!(principal.claims has team) || principal.claims.team == "x"as!(principal.claims has team) || (principal.claims has team && principal.claims.team == "x").the types Agent and Workspace are not compatible: a set mixes entity types, as inprincipal in [Workspace::"w", Agent::"a"]. Writeprincipal in Workspace::"w" || principal in Agent::"a".
See Schema validation.
Phase is Error with a spec.producers message
Section titled “Phase is Error with a spec.producers message”A producer the policy reads has a result that is not a bool, int or string, or whose type is not known until the call. A bare field read such as body.amount is untyped: convert it, as in int(body.amount).
A policy is ignored for one agent
Section titled “A policy is ignored for one agent”A broker compiles only the ToolPolicies whose spec.selector.registry matches its agent’s toolRegistryRef. Check that the names match.
Related pages
Section titled “Related pages”- ToolPolicy reference: every field, status and the denial format
- Policy engine architecture: how the broker decides and how changes reach it
- Securing agents with policies: end-to-end tutorial