# 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?

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

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).

```mermaid
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| TP
```

### 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: `permit` and `forbid` statements 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](#audit-logging)).

See the [ToolPolicy reference](/reference/policies/toolpolicy/) for every field. ToolPolicy is an [Enterprise](/explanation/platform/licensing/) feature.

## Context propagation

A policy that decides on who is calling and what they are calling needs identity and request context at every service boundary:

```mermaid
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 headers
```

### 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](/reference/platform/facade-runtime-protocol/) 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](/explanation/security/authentication/) 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

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

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](/how-to/security/configure-authentication/) for the field reference.

## 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

`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.
- `allow` permits 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 a `forbid` written as `context has x && context.x` would 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. A `forbid` that errors then stops refusing, and a `permit` beside it still allows the call. The broker denies rather than trust a decision reached without that statement.

## 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](/reference/policies/toolpolicy/#schema-validation).

## 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.

```json
{"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

- 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

- [ToolPolicy CRD Reference](/reference/policies/toolpolicy/): field-by-field specification (Enterprise)
- [Configure Tool Policies](/how-to/security/configure-tool-policies/): operational guide (Enterprise)
- [Securing Agents with Policies](/tutorials/securing-agents/): end-to-end tutorial
- [Facade and runtime protocol reference](/reference/platform/facade-runtime-protocol/): the full metadata contract