Policy engine architecture
Omnia’s policy engine decides, for every server-executed tool call an agent makes, whether the call may run. This page explains why that decision lives where it does, how identity reaches it, and what the trade-offs of the design are.
Why policies?
Section titled “Why policies?”An agent can invoke tools on behalf of users. Without a guardrail it could:
- Call a tool it should not have access to
- Call an allowed tool with arguments it should not send, such as a refund above a limit
- Act without knowing who the end user is
A ToolPolicy is a declarative, Kubernetes-native answer to each of these. Operators write it once and the platform enforces it on every call.
How tool policy is enforced
Section titled “How tool policy is enforced”The runtime asks a decision broker before it runs any tool. The following diagram shows the agent pod, where the runtime is the enforcement point (PEP) and the policy-broker sidecar is the decision point (PDP).
graph LR CLIENT[Client] --> FACADE[Facade] subgraph "Agent Pod" FACADE -->|gRPC| RUNTIME[Runtime<br/>PEP] RUNTIME -->|"POST /v1/decision"| BROKER[policy-broker :8090<br/>PDP] BROKER -->|"allow, deniedBy, injectedHeaders"| RUNTIME end RUNTIME -->|Allowed| TOOL[Tool Service] CTRL[Operator Controller] -->|Reconciles| TP[ToolPolicy CRD] BROKER -->|Watches| TPToolPolicy (application-level, Enterprise)
Section titled “ToolPolicy (application-level, Enterprise)”The runtime’s OmniaExecutor.dispatch is the single chokepoint that all four tool-executor types (HTTP, OpenAPI, gRPC, MCP) funnel through. Before the tool runs, it calls the policy-broker sidecar over POLICY_BROKER_URL (POST /v1/decision on port 8090 in the same pod) once per server-executed tool call. The broker watches ToolPolicy CRDs, evaluates the Cedar policy of each matching ToolPolicy against the request context and the caller’s identity, and returns a decision. The broker answers decision requests and never forwards tool traffic, so it sits outside the tool call’s request path.
A ToolPolicy and its broker provide:
- The Cedar policy:
permitandforbidstatements over a principal (the caller), an action and a resource (the tool). - CEL producers: each producer computes one named fact from the tool call, which the Cedar policy reads as
context.<name>. - Header injection: headers the runtime attaches to the outbound tool call, returned alongside the decision and only when the call is allowed.
- Fail-closed by default: if the broker is unreachable, the runtime denies the call. A deployment can opt into fail-open with
POLICY_BROKER_FAIL_MODE=open. - Audit logging: structured logs for denials and audit-mode would-denies (see Audit logging).
See the ToolPolicy reference for every field. ToolPolicy is an Enterprise feature.
Context propagation
Section titled “Context propagation”A policy that decides on who is calling and what they are calling needs identity and request context at every service boundary:
sequenceDiagram participant Client participant Facade participant Runtime participant Broker as policy-broker participant Tool as Tool Service
Client->>Facade: WebSocket + credential Facade->>Facade: Auth validator verifies JWT, builds AuthenticatedIdentity (origin, workspace, claims) Facade->>Runtime: gRPC + x-omnia-* metadata (identity + claims, Authorization withheld) Runtime->>Broker: POST /v1/decision (headers + body + reconstructed identity) Broker->>Broker: Evaluate Cedar policies against context + identity Broker->>Runtime: {allow, deniedBy, message, mode, wouldDeny, injectedHeaders} Runtime->>Tool: Forward (if allowed) + injected headersPropagated headers
Section titled “Propagated headers”The facade sets the agent name, namespace, request id, user pseudonym, email, admitting validator (x-omnia-origin), workspace and one x-omnia-claim-<name> header per verified claim. The runtime adds the provider type, model, tool name, tool registry and promoted scalar tool parameters (x-omnia-param-*). The Facade and runtime protocol reference lists every header.
The x-omnia-user-* headers always originate at the facade (pkg/policy/context.go). The facade fills them from its auth chain. Istio is optional and off by default (istio.enabled, authentication.enabled): the facade verifies tokens itself, as Authentication architecture describes. When the chart’s authentication.enabled gate is off and an Istio edge sits in front, the facade falls back to the edge’s x-user-id header.
The raw Authorization bearer token is never propagated outbound. Re-emitting the caller’s inbound token on a tool call would leak the user’s credential to arbitrary upstreams and overwrite a tool’s own authSecretRef credential. The token stays in the facade’s in-process context, and user identity reaches tools through the x-omnia-claim-* headers.
Identity in the decision
Section titled “Identity in the decision”The facade’s in-process AuthenticatedIdentity does not cross the facade to runtime gRPC hop. The runtime rebuilds it from the flat metadata (IdentityPayloadFromPropagation) as a structured object with origin, subject, endUser, workspace, agent and claims, and sends it on every decision request. Cedar policies read it as principal; CEL producers and header injection read it as identity.
Roles are an ordinary entry in claims, sourced from the edge’s role header, the IdP’s own role claim, or the API key’s stamped role, depending on which validator admitted the request. The role is the claims key role: tested with principal.claims has role and read as principal.claims.role.
JWT claim extraction
Section titled “JWT claim extraction”The facade forwards every claim the admitting validator put on the identity as an x-omnia-claim-<name> header. The runtime reads those back from gRPC metadata, so the keys of principal.claims are the lowercased metadata names: a claim Team is the key team, tested with principal.claims has team and read as principal.claims.team. A hyphenated name is quoted: principal.claims["customer-id"]. spec.externalAuth.oidc.claimMapping maps only subject and endUser; every other claim keeps its own name. See Configure Agent Authentication for the field reference.
Enforcement modes
Section titled “Enforcement modes”spec.mode sets whether a policy decides or only reports. Under enforce (the default), a refusal makes the broker return allow: false and the runtime aborts the tool dispatch. Under audit, the policy neither grants nor refuses. When it would have refused, the broker returns allow: true with wouldDeny: true, and the runtime proceeds and records it.
Cedar is default-deny, and permits add up across policies. One matching permit in any enforcing policy grants the call, and a condition inside a permit cannot withdraw a grant another policy gave. Only a forbid refuses. This keeps policies independent: a team can add a restriction by adding a forbid without editing anyone else’s permit.
Failure behavior
Section titled “Failure behavior”onFailure sets what happens when evaluation itself fails, either because a producer cannot compute its fact or because Cedar cannot evaluate a statement:
deny(default) treats the failure as a denial.allowpermits the request despite the failure.
Under deny the broker returns a denial whose deniedBy is producer-failed or evaluation-error, and each has a reason.
producer-failed: the broker fails the whole context build and never evaluates the policy. Going ahead without the fact would let a policy read it as absent, so aforbidwritten ascontext has x && context.xwould stop matching and the call would be permitted wider than the author wrote it. Denying keeps the call inside what the policy permits.evaluation-error: Cedar skips a statement it cannot evaluate. Aforbidthat errors then stops refusing, and apermitbeside it still allows the call. The broker denies rather than trust a decision reached without that statement.
How policy changes reach a broker
Section titled “How policy changes reach a broker”Each broker watches ToolPolicy resources in its namespace. An add, update or delete recompiles or evicts the policy in the running broker and applies to the next decision. No agent restart is needed.
The operator sets OMNIA_TOOL_REGISTRIES on the sidecar from the agent’s toolRegistryRef, and the broker compiles only the policies whose spec.selector.registry matches. An update that repoints a policy at another registry evicts the compiled copy. An agent with no toolRegistryRef compiles every ToolPolicy in its namespace.
When an edit fails to compile, a running broker keeps enforcing the previous version, and the operator reports Phase: Error on the resource. A broker that starts after the bad edit has no compiled version of that policy, so the policy grants nothing there.
The operator and the broker also validate the Cedar text against a schema generated from the policy’s producers and the built-in context. A statement that reads an undeclared context key, compares a key with the wrong type, or names an unknown entity type puts the resource in Phase: Error, and a running broker keeps enforcing the previous version, as for a compile failure. A broker with no previous version loads a schema-invalid policy that parses, so a restart does not leave the policy’s tools without a grant. See Schema validation.
Audit logging
Section titled “Audit logging”The policy-broker emits structured JSON logs for every denial and for every audit-mode would-deny. Two lines land per decision: a shared policy_decision line (outcome, mode, matched policy, message) and a broker_tool_decision line carrying toolName and toolRegistry. Every decision request is the same POST /v1/decision, so tool identity travels in dedicated fields.
{"msg":"policy_decision","allowed":true,"deniedBy":"max-refund-amount","message":"Refund amount exceeds $500 limit","mode":"audit","policy":"refund-limits"}{"msg":"broker_tool_decision","toolName":"process_refund","toolRegistry":"customer-tools","allowed":true,"deniedBy":"max-refund-amount","mode":"audit"}In audit mode a matched forbid sets deniedBy and message but allowed stays true; under enforce the same match produces allowed: false. An audit-mode policy that holds only forbid statements reports a would-deny on every call it selects, because nothing in it permits the call. Include a permit in an audit-mode policy to get a would-deny only when a forbid matches.
Logging is unconditional: the broker emits the pair for every denial and would-deny, and skips only plain allows. It does not redact body or headers values, so keep sensitive data out of the fields your producers read if broker-log exposure is a concern.
Trade-offs and alternatives
Section titled “Trade-offs and alternatives”- A called broker serves better than a proxy. Omnia runs Istio in ambient mode, which has no waypoint proxy on tool egress, so a reverse proxy sitting passively in the network path would never see the traffic. Calling the broker directly avoids transparent interception. The cost is one in-pod HTTP round trip per tool call, with a 2-second client timeout.
- Fail-closed is the default because an enforcement layer that silently allows when its decision service is down gives no protection exactly when something is wrong. Fail-open exists for deployments that value tool availability over enforcement, and it is a per-deployment choice.
- Each agent pod gets its own sidecar. Each broker holds only the ToolPolicies of its own namespace, filtered to the agent’s ToolRegistry. Decisions need no network hop outside the pod, and a policy change reaches a broker through its watch.
- Cedar decides and CEL computes facts. Cedar keeps the grant and refusal logic analysable, and CEL producers keep arbitrary computation out of it. The model writes a tool call’s arguments, so a producer is where untrusted input is checked, and the policy trusts the facts the producers report. A policy cannot read raw tool arguments, so an author must name each fact a producer computes.
Related resources
Section titled “Related resources”- ToolPolicy CRD Reference: field-by-field specification (Enterprise)
- Configure Tool Policies: operational guide (Enterprise)
- Securing Agents with Policies: end-to-end tutorial
- Facade and runtime protocol reference: the full metadata contract