Policy Configuration
Manetu Policy Engine (MPE) is a policy evaluation engine that uses Rego, the policy language from Open Policy Agent (OPA). AgentVisor™ uses MPE to enforce fine-grained authorization controls, determining which HTTP endpoints, tools, and resources each agent can access based on the caller's identity.
This guide covers writing and configuring MPE PolicyDomains for AgentVisor.
PolicyDomain Basics
Policies are defined in YAML using MPE's v1beta1 schema:
apiVersion: iamlite.manetu.io/v1beta1
kind: PolicyDomain
metadata:
name: my-policies
spec:
policies:
- mrn: "mrn:iam:policy:allow-authenticated"
name: allow-authenticated
rego: |
package authz
default allow = false
allow { input.principal.sub != "" }
resource-groups:
- mrn: "mrn:iam:resource-group:allowed"
name: allowed
policy: "mrn:iam:policy:allow-authenticated"
resources:
- name: example
selector: ["mrn:agentvisor:http:example\\.com.*"]
group: "mrn:iam:resource-group:allowed"
It's missing the operations: and roles: sections every real domain
needs — see Complete Example below for one that
actually loads and grants/denies traffic. It's also missing something
easy to miss: input.principal.sub != "" is true for the built-in
anonymous principal too (sub: "anonymous"), not just for callers
with real credentials — see Unauthenticated Requests and the Anonymous
Principal
for why, and how to tighten it.
MRN Format Conventions
AgentVisor uses two MRN (Manetu Resource Name) namespaces:
| Namespace | Format | Purpose |
|---|---|---|
iam | mrn:iam:<type>:<name> | IAM resources: policies, resource groups, roles |
agentvisor | mrn:agentvisor:<type>:<identifier> | Runtime resources: threads, agents, HTTP endpoints |
IAM namespace (mrn:iam:) — Used for policy infrastructure that defines how authorization works:
mrn:iam:policy:<name>— Policy definitionsmrn:iam:resource-group:<name>— Collections of resources sharing a policy (uses hyphen)
AgentVisor namespace (mrn:agentvisor:) — Used for actual resources being protected:
mrn:agentvisor:http:<host>/<path>— HTTP endpointsmrn:agentvisor:thread:<id>— Conversation threadsmrn:agentvisor:tool:<name>— Agent toolsmrn:agentvisor:resourcegroup:<name>— Resource group references in policies (no hyphen)
The iam namespace uses resource-group (hyphenated), while the agentvisor namespace uses resourcegroup (no hyphen). This reflects the origin of each schema—follow the examples in this guide for correct formatting.
Core Components
Policies
Rego rules that evaluate to allow or deny:
policies:
# Allow any authenticated principal (this also grants the anonymous
# principal, sub: "anonymous" — see the note above)
- mrn: "mrn:iam:policy:allow-authenticated"
name: allow-authenticated
rego: |
package authz
default allow = false
allow { input.principal.sub != "" }
# Deny everything
- mrn: "mrn:iam:policy:deny-all"
name: deny-all
rego: |
package authz
default allow = false
# Allow specific roles
- mrn: "mrn:iam:policy:admin-only"
name: admin-only
rego: |
package authz
default allow = false
allow { input.principal.role == "admin" }
Resource Groups
Collections of resources sharing a policy:
resource-groups:
- mrn: "mrn:iam:resource-group:llm-apis"
name: llm-apis
policy: "mrn:iam:policy:allow-authenticated"
- mrn: "mrn:iam:resource-group:blocked"
name: blocked
policy: "mrn:iam:policy:deny-all"
- mrn: "mrn:iam:resource-group:admin-resources"
name: admin-resources
policy: "mrn:iam:policy:admin-only"
Resources
Map MRN patterns to groups using regex selectors:
resources:
- name: openai
selector: ["mrn:agentvisor:http:api\\.openai\\.com.*"]
group: "mrn:iam:resource-group:llm-apis"
- name: anthropic
selector: ["mrn:agentvisor:http:api\\.anthropic\\.com.*"]
group: "mrn:iam:resource-group:llm-apis"
- name: internal-networks
selector:
- "mrn:agentvisor:http:10\\..*"
- "mrn:agentvisor:http:192\\.168\\..*"
group: "mrn:iam:resource-group:blocked"
Common Patterns
Allow LLM APIs
spec:
resources:
# OpenAI
- name: openai
selector: ["mrn:agentvisor:http:api\\.openai\\.com.*"]
group: "mrn:iam:resource-group:allowed"
# Anthropic
- name: anthropic
selector: ["mrn:agentvisor:http:api\\.anthropic\\.com.*"]
group: "mrn:iam:resource-group:allowed"
# Google AI
- name: google-ai
selector: ["mrn:agentvisor:http:generativelanguage\\.googleapis\\.com.*"]
group: "mrn:iam:resource-group:allowed"
# Hugging Face
- name: huggingface
selector: ["mrn:agentvisor:http:api-inference\\.huggingface\\.co.*"]
group: "mrn:iam:resource-group:allowed"
Block Internal Networks
spec:
resources:
# RFC1918 private networks
- name: private-10
selector: ["mrn:agentvisor:http:10\\..*"]
group: "mrn:iam:resource-group:blocked"
- name: private-172
selector: ["mrn:agentvisor:http:172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"]
group: "mrn:iam:resource-group:blocked"
- name: private-192
selector: ["mrn:agentvisor:http:192\\.168\\..*"]
group: "mrn:iam:resource-group:blocked"
# Localhost
- name: localhost
selector:
- "mrn:agentvisor:http:localhost.*"
- "mrn:agentvisor:http:127\\..*"
group: "mrn:iam:resource-group:blocked"
# Cloud metadata services
- name: aws-metadata
selector: ["mrn:agentvisor:http:169\\.254\\.169\\.254.*"]
group: "mrn:iam:resource-group:blocked"
- name: gcp-metadata
selector: ["mrn:agentvisor:http:metadata\\.google\\.internal.*"]
group: "mrn:iam:resource-group:blocked"
Allow Specific Paths
spec:
resources:
# Allow only chat completions, not fine-tuning
- name: openai-chat
selector: ["mrn:agentvisor:http:api\\.openai\\.com/v1/chat.*"]
group: "mrn:iam:resource-group:allowed"
# Block fine-tuning endpoints
- name: openai-finetune
selector: ["mrn:agentvisor:http:api\\.openai\\.com/v1/fine.*"]
group: "mrn:iam:resource-group:blocked"
Multi-Tenant Policies
spec:
policies:
- mrn: "mrn:iam:policy:tenant-match"
name: tenant-match
rego: |
package authz
default allow = false
# Allow if principal's tenant matches resource tenant
allow {
input.principal.tenant == input.context.resource_tenant
}
# Admins can access all tenants
allow {
input.principal.role == "admin"
}
resource-groups:
- mrn: "mrn:iam:resource-group:tenant-resources"
name: tenant-resources
policy: "mrn:iam:policy:tenant-match"
Default Deny
Always include a catch-all deny:
spec:
resources:
# Specific allows first
- name: allowed-api
selector: ["mrn:agentvisor:http:api\\.allowed\\.com.*"]
group: "mrn:iam:resource-group:allowed"
# Catch-all deny MUST be last
- name: default-http
selector: ["mrn:agentvisor:http:.*"]
group: "mrn:iam:resource-group:blocked"
AgentVisor Core Resources
Threads, runs, state, agents, and store items are not matched by
selector. The host sends these to MPE as fully-qualified resource
descriptors (an MRN plus owner/group already attached — see
Policy Enforcement for the phase
details), so MPE never consults resources: for them. Instead, define
resource-groups: entries whose MRNs match these five constants exactly
(they come from pkg/authz/resource.go in the AgentVisor source and
will not resolve if misspelled or given the iam namespace instead):
spec:
resource-groups:
- mrn: "mrn:agentvisor:resourcegroup:threads"
name: threads
policy: "mrn:iam:policy:allow-authenticated"
- mrn: "mrn:agentvisor:resourcegroup:runs"
name: runs
policy: "mrn:iam:policy:allow-authenticated"
- mrn: "mrn:agentvisor:resourcegroup:state"
name: state
policy: "mrn:iam:policy:allow-authenticated"
- mrn: "mrn:agentvisor:resourcegroup:agents"
name: agents
policy: "mrn:iam:policy:allow-authenticated"
- mrn: "mrn:agentvisor:resourcegroup:store"
name: store
policy: "mrn:iam:policy:allow-authenticated"
SaveCheckpoint/LoadCheckpoint deliberately skip authorization
entirely — they're internal to a run whose access was already
adjudicated at run creation, so there's no
mrn:agentvisor:resourcegroup:checkpoint to define. See Policy
Enforcement
for the full list of which operations are and aren't evaluated.
Complete Example
This domain actually loads and grants/denies traffic as described — it's
adapted from the working policy shipped with the langgraph/chatbot-agent
template (agentvisor template create langgraph/chatbot-agent). Unlike the
fragments above, it includes the two sections every real domain needs:
operations: (without it, MPE hard-denies every request before resource
policies ever run) and roles: (without a role matching the caller's
mroles/mgroups claims, MPE denies every request regardless of what the
resource policy would decide — see Policy
Enforcement for why both phases exist).
apiVersion: iamlite.manetu.io/v1beta1
kind: PolicyDomain
metadata:
name: production-agent
spec:
policies:
# Operation phase - tri-state (-1 deny, 0 continue to the other
# phases, >0 grant and skip them). Runs before every other phase.
- mrn: &policy-operation "mrn:iam:policy:agentvisor-operation"
name: agentvisor-operation
rego: |
package authz
import rego.v1
default allow := -1
allow := 0 if input.principal.sub != ""
# This also grants the built-in anonymous principal (sub: "anonymous") —
# see Unauthenticated Requests and the Anonymous Principal in Policy
# Enforcement. Add `input.principal.sub != "anonymous"` to require real
# credentials.
- mrn: &policy-allow "mrn:iam:policy:allow-authenticated"
name: allow-authenticated
rego: |
package authz
import rego.v1
default allow := false
allow if input.principal.sub != ""
- mrn: &policy-deny "mrn:iam:policy:deny-all"
name: deny-all
rego: |
package authz
default allow = false
# Identity phase - the anonymous principal automatically carries the
# "mrn:agentvisor:role:anonymous" role (see pkg/authz/principal.go), so
# this role is what lets it pass. A real OIDC deployment would add a role
# here for whatever "mroles" value its identity provider issues instead —
# see the OIDC Setup guide.
roles:
- mrn: "mrn:agentvisor:role:anonymous"
name: anonymous
policy: *policy-allow
resource-groups:
# Core AgentVisor resources - MRNs must match pkg/authz/resource.go
# exactly; see AgentVisor Core Resources above for why no `resources:`
# selector is needed for these.
- mrn: "mrn:agentvisor:resourcegroup:threads"
name: threads
policy: *policy-allow
- mrn: "mrn:agentvisor:resourcegroup:runs"
name: runs
policy: *policy-allow
- mrn: "mrn:agentvisor:resourcegroup:state"
name: state
policy: *policy-allow
- mrn: "mrn:agentvisor:resourcegroup:agents"
name: agents
policy: *policy-allow
- mrn: "mrn:agentvisor:resourcegroup:store"
name: store
policy: *policy-allow
- mrn: "mrn:iam:resource-group:llm-apis"
name: llm-apis
policy: *policy-allow
- mrn: "mrn:iam:resource-group:blocked"
name: blocked
policy: *policy-deny
- mrn: "mrn:iam:resource-group:default"
name: default
default: true
policy: *policy-deny
resources:
# HTTP endpoints arrive as simple MRN strings (the host doesn't
# pre-resolve a group for them), so they're matched by selector here.
# Allowed LLM APIs
- name: openai
selector: ["mrn:agentvisor:http:api\\.openai\\.com.*"]
group: "mrn:iam:resource-group:llm-apis"
- name: anthropic
selector: ["mrn:agentvisor:http:api\\.anthropic\\.com.*"]
group: "mrn:iam:resource-group:llm-apis"
# Blocked networks
- name: internal-10
selector: ["mrn:agentvisor:http:10\\..*"]
group: "mrn:iam:resource-group:blocked"
- name: internal-172
selector: ["mrn:agentvisor:http:172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"]
group: "mrn:iam:resource-group:blocked"
- name: internal-192
selector: ["mrn:agentvisor:http:192\\.168\\..*"]
group: "mrn:iam:resource-group:blocked"
- name: metadata
selector: ["mrn:agentvisor:http:169\\.254\\..*"]
group: "mrn:iam:resource-group:blocked"
# Default deny - must be last
- name: default-http
selector: ["mrn:agentvisor:http:.*"]
group: "mrn:iam:resource-group:default"
# Operation phase - a single entry routes every AgentVisor operation
# through the tri-state policy above.
operations:
- name: agentvisor-ops
selector: ["agentvisor:.*"]
policy: *policy-operation
This example grants the anonymous principal — appropriate for local
development (--sandbox=none, no api.auth configured). Save it as
policies/domain.yml, verify it with mpe lint -f ./policies/domain.yml,
then a real request:
agentvisor serve . --sandbox=none --policy ./policies/domain.yml &
curl -X POST http://localhost:8090/threads # 201 Created
Connecting to a Remote PDP
The http provider delegates policy decisions to an external Policy Decision Point (PDP) over HTTP/HTTPS. Use this when:
- You run a centralized or multi-tenant policy service.
- Your PDP is not co-located on the same host as AgentVisor (i.e., it doesn't follow the MPE-Premium sidecar model).
- You need to integrate with a third-party OPA/PDP service.
Minimal HTTPS configuration
The simplest secure setup uses a custom CA to verify the PDP's certificate:
authz:
type: http
http:
url: "https://pdp.example.com:9000"
tls:
mode: verify-full # default when https:// URL is used
ca_file: "/etc/agentvisor/pdp-ca.pem"
Equivalent environment variables:
export AGENTVISOR_AUTHZ_TYPE=http
export AGENTVISOR_AUTHZ_HTTP_URL=https://pdp.example.com:9000
export AGENTVISOR_AUTHZ_HTTP_TLS_MODE=verify-full
export AGENTVISOR_AUTHZ_HTTP_TLS_CA_FILE=/etc/agentvisor/pdp-ca.pem
Mutual TLS (mTLS)
When the PDP requires client certificate authentication:
authz:
type: http
http:
url: "https://pdp.example.com:9000"
tls:
mode: verify-full
ca_file: "/etc/agentvisor/pdp-ca.pem"
cert_file: "/etc/agentvisor/client.pem"
key_file: "/etc/agentvisor/client-key.pem"
Inline certificate data (base64-encoded PEM) is also supported via ca_data, cert_data, and key_data — useful in containerized deployments where mounting files is inconvenient.
Custom headers (API keys, bearer tokens, tenant IDs)
Use the headers list to inject arbitrary headers on every PDP request. Reference secrets from environment variables using value_env to avoid committing credentials to version control:
authz:
type: http
http:
url: "https://pdp.example.com:9000"
tls:
mode: verify-full
ca_file: "/etc/agentvisor/pdp-ca.pem"
headers:
- name: "X-Tenant-ID"
value: "acme" # static value
- name: "Authorization"
value_env: "PDP_TOKEN" # reads $PDP_TOKEN from the environment at startup
Note: The
headerslist can only be set via YAML configuration file, not via flat environment variables. Setvalue_envto an environment variable name to keep secrets out of your config files.
Development / insecure mode
For local development with self-signed certificates, set mode: require under tls: to skip all verification. Never use this in production.
authz:
type: http
http:
url: "https://localhost:9000"
tls:
mode: require # ⚠️ skips ALL certificate verification — dev/test only
TLS mode reference
| Mode | Behavior | Use case |
|---|---|---|
verify-full | Verify cert chain + hostname | Production (default for https://) |
verify-ca | Verify cert chain, skip hostname | Internal services with wildcard certs or proxies |
require | TLS on, skip all verification | Development / self-signed without CA distribution |
disable | No TLS (http:// only) | Localhost / sidecar on same node |
Security guidance
- Prefer
verify-fullin production. - Use
ca_file(orca_data) to pin a specific internal CA rather than relying on system roots. - For bearer token or API key authentication, always use
value_envinstead of inlinevalueto avoid credentials appearing in config files or logs. - Use mTLS (
cert_file+key_file) when the PDP enforces client identity.
Testing Policies
The mpe CLI tool validates and tests PolicyDomain files before deployment. It's part of the Manetu PolicyEngine project.
Installation: See the MPE documentation for installation instructions.
Validate Syntax
mpe lint -f ./policies/domain.yml
Run with Debug Logging
AGENTVISOR_LOG_LEVEL=debug agentvisor serve ./my-agent
Test Specific Requests
# Should be allowed
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=30s" \
-d '{"input": {"url": "http://api.openai.com/v1/models"}}' | jq
# Should be blocked
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=30s" \
-d '{"input": {"url": "http://192.168.1.1/"}}' | jq
Multiple Policy Files
Combine multiple policy files. --policy is a single flag, so passing it
more than once doesn't merge the values — only the last occurrence takes
effect. Use a comma-separated list instead:
agentvisor serve ./my-agent \
--policy ./policies/base.yml,./policies/custom.yml
Or via environment variables:
export AGENTVISOR_AUTHZ_TYPE=embedded
export AGENTVISOR_AUTHZ_EMBEDDED_POLICY_DOMAIN_FILES=./policies/base.yml,./policies/custom.yml